From 24e27adb8ffcafe1f5cb5c57d53d7f558bd5617b Mon Sep 17 00:00:00 2001 From: Jarian Cottingham Date: Thu, 16 Oct 2025 02:02:10 -0500 Subject: [PATCH] Website live --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 9 ++ README.md | 89 ++++++++++++++++ app.py | 244 +++++++++++++++++++++++++++++++++++++++++++ load_snapshot.sh | 2 + requirements.txt | 2 + static/css/style.css | 127 ++++++++++++++++++++++ static/js/script.js | 114 ++++++++++++++++++++ templates/index.html | 26 +++++ 9 files changed, 613 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 load_snapshot.sh create mode 100644 requirements.txt create mode 100644 static/css/style.css create mode 100644 static/js/script.js create mode 100644 templates/index.html diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..66544f017c880eea9e05fd2d544a58a0a328c19d GIT binary patch literal 6148 zcmeHKOKQU~5S>X)F?8c)m%2i3AWV9KT%aX^PzWxxN!DKVTsd0ad^X1I&e?=FFnZEx zo`l}Q;}H?<&-=AVCn7D}P=0Ngo9&y=Y?ToO!g0o@>@M5y;rKNiX4!uSjQcD4-ZW$e#hzg+>MLt-uXFauu5Z literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52a852e --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Created by venv; see https://docs.python.org/3/library/venv.html +__pycache__ +bin/ +include/ +lib/ +pyvenv.cfg + +images/ +files_snapshot.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..a71bf84 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# Space Facts Viewer + +A mobile-optimized picture viewer app that displays space images with interesting facts about each image. Click on an image to toggle the visibility of the associated fact. + +## Features + +- Full-screen image display with overlaying facts +- Toggle functionality - click image to show/hide facts +- Previous/Next navigation buttons +- Mobile-responsive design optimized for touch interaction +- Clean, space-themed dark interface + +## Installation + +1. Install Python Flask: + ``` + pip install flask + ``` + +2. Ensure you have the required directory structure: + ``` + SpaceFacts/ + ├── app.py + ├── requirements.txt + ├── templates/ + │ └── index.html + ├── static/ + │ ├── css/ + │ │ └── style.css + │ ├── js/ + │ │ └── script.js + │ └── images/ # Placeholder images already included + ``` + +## Usage + +1. The app is already configured to use the placeholder images in the `static/images/` directory: + - `20250331-04~orig.jpg` + - `AFRC2025-0036-26~orig.jpg` + - `CEB_1408~orig.jpg` + - `iss072e742575~orig.jpg` + - `iss073e0000706~orig.jpg` + +2. Run the Flask application: + ``` + python app.py + ``` + +3. Open your browser and navigate to `http://localhost:6007` + +## How It Works + +- **Click on any image** to toggle the visibility of the fact overlay +- Use the **Previous/Next buttons** to navigate between different space facts +- The interface is fully responsive and optimized for mobile devices +- Images are displayed in full-screen mode with facts overlaid at the bottom + +## Technical Details + +### Backend (Flask) +The Flask app provides REST API endpoints: +- `/` - Main application page +- `/api/facts` - JSON list of all space facts +- `/api/fact/` - JSON for a specific fact +- `/api/image/` - Returns the image file for a fact + +### Frontend +- Responsive design using CSS media queries +- JavaScript handles click events and navigation +- Smooth slide animations for overlay toggling +- Full-screen canvas with proper aspect ratio handling + +## Customization + +To add more facts: +1. Add entries to the `SPACE_FACTS` array in `app.py` +2. Ensure corresponding image files exist in `static/images/` + +## Mobile Optimization + +The application is optimized for mobile devices with: +- Touch-friendly button sizes +- Responsive layout adjustments +- Single-tap interaction for toggling facts +- Optimized viewport settings + +## License + +This project is built for educational purposes and can be modified freely. \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..42faabb --- /dev/null +++ b/app.py @@ -0,0 +1,244 @@ +from flask import Flask, render_template, send_file, jsonify +import os +import random +import json + +app = Flask(__name__) + +# Configuration for NASA data paths - using environment variables with defaults +METADATA_PATH = os.environ.get( + "NASA_METADATA_PATH", "/Volumes/MediaServer/Images/NASA/metadata" +) +IMAGES_PATH = os.environ.get( + "NASA_IMAGES_PATH", "/Volumes/MediaServer/Images/NASA/images" +) +SNAPSHOT_FILE = os.environ.get("SNAPSHOT_FILE", "./files_snapshot.txt") + +# History tracking +image_history = [] +MAX_HISTORY = 50 + + +def load_snapshot(): + """Load the snapshot file to get all available image metadata files""" + try: + if not os.path.exists(SNAPSHOT_FILE): + return [] + + with open(SNAPSHOT_FILE, "r") as f: + lines = [line.strip() for line in f.readlines() if line.strip()] + return lines + except Exception as e: + print(f"Error loading snapshot: {e}") + return [] + + +def get_random_image_metadata(): + """Get a random image from the snapshot""" + snapshot_files = load_snapshot() + if not snapshot_files: + return None + + selected_file = random.choice(snapshot_files) + return selected_file + + +def get_image_data(metadata_filename): + """Extract image data from metadata file""" + try: + # Construct full path to metadata file + metadata_path = os.path.join(METADATA_PATH, metadata_filename) + + if not os.path.exists(metadata_path): + return None + + with open(metadata_path, "r") as f: + metadata = json.load(f) + + # Extract the image name from metadata filename (remove index- prefix and .json suffix) + base_name = metadata_filename[6:-5] # Remove "index-" prefix and ".json" suffix + + # Construct full path to image file + image_filename = f"{base_name}~orig.jpg" + image_path = os.path.join(IMAGES_PATH, image_filename) + + # Parse data structure - it's an array, so get first element + description = "" + title = base_name + + if ( + "data" in metadata + and isinstance(metadata["data"], list) + and len(metadata["data"]) > 0 + ): + data_item = metadata["data"][0] # Get the first item from the array + + if isinstance(data_item, dict): + # Look for description in the first data element + if "description" in data_item: + description = data_item["description"] + elif "abstract" in data_item: + description = data_item["abstract"] + elif "title" in data_item: + title = data_item["title"] + + # Additional fallbacks + if not description and isinstance(data_item, dict): + for key, value in data_item.items(): + if ( + isinstance(value, str) + and len(value) > 20 + and not key.startswith("_") + ): + if len(value) > 100 or ( + len(value) > 50 and "space" in value.lower() + ): + description = value + break + + # If no description from data array, try root level + if not description and "description" in metadata: + description = metadata["description"] + + # If still no description, try looking for long strings at root level + if not description and isinstance(metadata, dict): + for key, value in metadata.items(): + if isinstance(value, str) and len(value) > 100: + description = value + break + + # Final fallback + if not description: + description = "No description available" + + return { + "id": base_name, + "title": title, + "description": description, + "image_path": image_path, + } + except Exception as e: + print(f"Error processing metadata {metadata_filename}: {e}") + return None + + +def add_to_history(image_data): + """Add image to history with limit""" + global image_history + + # Only add if not already in history + if not any(item["id"] == image_data["id"] for item in image_history): + image_history.append(image_data) + + # Limit history size + if len(image_history) > MAX_HISTORY: + image_history.pop(0) + + +def get_next_image(): + """Get the next image in history or a random one if at end""" + if not image_history: + return None + + # Return random image instead of going to next in history + # This ensures we always generate new images when at the end + metadata_filename = get_random_image_metadata() + if metadata_filename: + image_data = get_image_data(metadata_filename) + return image_data + + return None + + +def get_previous_image(): + """Get previous image from history""" + global image_history + + if len(image_history) >= 2: + # Return the second-to-last item (previous in history) + return image_history[-2] + elif len(image_history) == 1: + return image_history[0] # Return current if only one in history + else: + return None + + +@app.route("/") +def index(): + """Main page - get initial random image""" + metadata_filename = get_random_image_metadata() + if metadata_filename: + image_data = get_image_data(metadata_filename) + if image_data: + add_to_history(image_data) + return render_template("index.html", facts=[image_data]) + + # Fallback to empty page + return render_template("index.html", facts=[]) + + +@app.route("/api/facts") +def get_facts(): + """Return current fact - for backwards compatibility""" + if image_history: + return jsonify([image_history[-1]]) + return jsonify([]) + + +@app.route("/api/fact/") +def get_fact(fact_id): + """Get a specific fact by ID - this is primarily for demo purposes""" + # In our implementation, we don't actually support this since we're generating random images + return jsonify({"error": "Not implemented in this version"}), 404 + + +@app.route("/api/image/") +def get_image(fact_id): + """Get image by ID""" + try: + # This is a bit tricky because our structure is different now + # We'll find the metadata file with that id and return its image + image_filename = f"{fact_id}~orig.jpg" + full_image_path = os.path.join(IMAGES_PATH, image_filename) + + if os.path.exists(full_image_path): + return send_file(full_image_path) + else: + return "Image not found", 404 + except Exception as e: + return f"Error: {e}", 500 + + +@app.route("/api/random-image") +def get_random_image(): + """Get a random image with metadata""" + metadata_filename = get_random_image_metadata() + if metadata_filename: + image_data = get_image_data(metadata_filename) + if image_data: + add_to_history(image_data) + return jsonify(image_data) + return jsonify({"error": "No images available"}), 404 + + +@app.route("/api/next-image") +def get_next_image_route(): + """Get next image (random)""" + next_image = get_next_image() + if next_image: + add_to_history(next_image) + return jsonify(next_image) + return jsonify({"error": "No more images"}), 404 + + +@app.route("/api/previous-image") +def get_previous_image_route(): + """Get previous image from history""" + prev_image = get_previous_image() + if prev_image: + return jsonify(prev_image) + return jsonify({"error": "No previous image"}), 404 + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=6008) diff --git a/load_snapshot.sh b/load_snapshot.sh new file mode 100644 index 0000000..6c2d53e --- /dev/null +++ b/load_snapshot.sh @@ -0,0 +1,2 @@ +ls /Volumes/MediaServer/Images/NASA/metadata >> files_snapshot.txt +ls /mnt/centralstoragemedia/Images/NASA/metadata >> files_snapshot.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5faa4fb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==2.3.3 +Werkzeug==2.3.7 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..8fdd4da --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,127 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background-color: #000; + color: #fff; + overflow: hidden; + height: 100vh; +} + +/* Main container */ +.container { + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; +} + +/* Image container with overlay */ +.image-container { + position: relative; + flex: 1; + width: 100%; + height: 100%; + overflow: hidden; + cursor: pointer; +} + +.space-image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.fact-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); + padding: 20px; + transform: translateY(0); + transition: transform 0.3s ease; + pointer-events: none; /* Allow clicks to pass through to image */ +} + +.fact-overlay.hidden { + transform: translateY(100%); +} + +#factTitle { + font-size: 1.5rem; + margin-bottom: 10px; + text-align: center; + color: white; +} + +#factDescription { + font-size: 1rem; + line-height: 1.4; + text-align: center; + color: white; +} + +/* Controls */ +.controls { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + background-color: rgba(0, 0, 0, 0.7); + gap: 20px; + z-index: 10; +} + +button { + background-color: #333; + color: white; + border: none; + padding: 12px 24px; + font-size: 1rem; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; + pointer-events: auto; /* Allow button clicks */ +} + +button:hover { + background-color: #555; +} + +/* Mobile optimization */ +@media (max-width: 768px) { + #factTitle { + font-size: 1.2rem; + } + + #factDescription { + font-size: 0.9rem; + } + + button { + padding: 10px 20px; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + .controls { + flex-direction: column; + gap: 10px; + } + + #factTitle { + font-size: 1rem; + } + + #factDescription { + font-size: 0.8rem; + } +} diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..b0dea07 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,114 @@ +// Global variables +let currentFact = null; +let imageHistory = []; + +// DOM elements +const imageContainer = document.getElementById("imageContainer"); +const spaceImage = document.getElementById("spaceImage"); +const factOverlay = document.getElementById("factOverlay"); +const factTitle = document.getElementById("factTitle"); +const factDescription = document.getElementById("factDescription"); +const prevBtn = document.getElementById("prevBtn"); +const nextBtn = document.getElementById("nextBtn"); + +// Fetch random image from API +async function fetchRandomImage() { + try { + const response = await fetch("/api/random-image"); + const imageData = await response.json(); + + if (imageData && !response.error) { + currentFact = imageData; + loadFact(currentFact); + // Add to history + imageHistory.push(currentFact); + + // Limit history to 50 items + if (imageHistory.length > 50) { + imageHistory.shift(); + } + } else { + console.error("Failed to fetch random image"); + } + } catch (error) { + console.error("Error fetching random image:", error); + } +} + +// Load a specific fact +function loadFact(fact) { + if (!fact) return; + + spaceImage.src = `/api/image/${fact.id}`; + factTitle.textContent = fact.title; + factDescription.textContent = fact.description; + + // Make sure overlay is visible when loading new image + factOverlay.classList.remove("hidden"); +} + +// Toggle fact overlay visibility +function toggleFactOverlay() { + if (factOverlay) { + factOverlay.classList.toggle("hidden"); + } +} + +// Load next image +async function loadNextImage() { + try { + const response = await fetch("/api/next-image"); + const imageData = await response.json(); + + if (imageData && !response.error) { + currentFact = imageData; + loadFact(currentFact); + + // Add to history + imageHistory.push(currentFact); + + // Limit history to 50 items + if (imageHistory.length > 50) { + imageHistory.shift(); + } + } else { + console.error("Failed to fetch next image"); + } + } catch (error) { + console.error("Error fetching next image:", error); + } +} + +// Load previous image from history +async function loadPreviousImage() { + if (imageHistory.length < 2) { + // If we're at the beginning, go back to random image + await fetchRandomImage(); + return; + } + + // Remove current item from history and get previous one + imageHistory.pop(); // Remove current + const prevFact = imageHistory[imageHistory.length - 1]; + + if (prevFact) { + currentFact = prevFact; + loadFact(currentFact); + } +} + +// Event listeners +imageContainer.addEventListener("click", toggleFactOverlay); + +prevBtn.addEventListener("click", () => { + loadPreviousImage(); +}); + +nextBtn.addEventListener("click", () => { + loadNextImage(); +}); + +// Initialize the app +document.addEventListener("DOMContentLoaded", () => { + fetchRandomImage(); +}); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..1fde47b --- /dev/null +++ b/templates/index.html @@ -0,0 +1,26 @@ + + + + + + Space Facts Viewer + + + +
+
+ Space Image +
+

+

+
+
+
+ + +
+
+ + + +