From 9ddf30610b0240a924abcdaba96fad6c1bb5efe2 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Wed, 11 Jun 2025 20:28:45 +1000 Subject: [PATCH] Update image gallary --- links/image_api.py | 40 +++++++++--- .../links/mini_apps/image_gallery.html | 63 +++++++++++++------ 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/links/image_api.py b/links/image_api.py index 7ab5a8e..3f4ce35 100644 --- a/links/image_api.py +++ b/links/image_api.py @@ -2,6 +2,8 @@ import os import random import logging import time +import threading +from collections import deque from pathlib import Path from typing import Optional, Tuple, Literal, List from PIL import Image @@ -26,6 +28,10 @@ MAX_RANDOM_ATTEMPTS = 30 # Maximum number of attempts to find a valid random im MINIMUM_IMAGE_SIZE = 300 # Minimum width and height for valid images executor = ThreadPoolExecutor(max_workers=4) +# Queue to track recently served images (thread-safe) +recent_images_queue = deque(maxlen=50) +recent_images_lock = threading.Lock() + # Initialize random seed for true randomness random.seed(time.time()) @@ -205,7 +211,7 @@ def resize_image(image: Image.Image, width: Optional[int], height: Optional[int] return image.resize((int(orig_width * ratio), height), Image.Resampling.LANCZOS) def get_supported_image_path() -> Optional[Path]: - """Get a random valid image path from root folder only, without loading all files into memory.""" + """Get a random valid image path from root folder only, avoiding recently served images.""" if not IMAGES_FOLDER or not os.path.exists(IMAGES_FOLDER): logger.warning(f"Images folder not found or not configured: {IMAGES_FOLDER}") return None @@ -213,6 +219,10 @@ def get_supported_image_path() -> Optional[Path]: base_path = Path(IMAGES_FOLDER) attempts = 0 + # Get current list of recent images (thread-safe) + with recent_images_lock: + recent_images = set(recent_images_queue) + while attempts < MAX_RANDOM_ATTEMPTS: attempts += 1 @@ -221,11 +231,21 @@ def get_supported_image_path() -> Optional[Path]: files_in_dir = [] for item in base_path.iterdir(): if item.is_file() and item.suffix.lower() in SUPPORTED_FORMATS: - files_in_dir.append(item) + # Skip images that were recently served + if item.name not in recent_images: + files_in_dir.append(item) if not files_in_dir: - logger.warning("No image files found in root folder") - return None + # If no non-recent images available, clear the recent queue and try again + if recent_images and attempts == 1: + logger.info("All available images were recently served, clearing recent images queue") + with recent_images_lock: + recent_images_queue.clear() + recent_images = set() + continue + else: + logger.warning("No image files found in root folder") + return None # Ensure truly random selection random.seed(time.time() + hash(str(os.urandom(8)))) @@ -237,8 +257,13 @@ def get_supported_image_path() -> Optional[Path]: if selected_file.exists() and is_valid_image_fast(selected_file): file_size_mb = selected_file.stat().st_size / (1024 * 1024) - logger.info(f"Selected random image: file='{selected_file.name}', " - f"size={file_size_mb:.2f}MB, attempt={attempts}/{MAX_RANDOM_ATTEMPTS}") + # Add to recent images queue (thread-safe) + with recent_images_lock: + recent_images_queue.append(selected_file.name) + + logger.debug(f"Selected random image: file='{selected_file.name}', " + f"size={file_size_mb:.2f}MB, attempt={attempts}/{MAX_RANDOM_ATTEMPTS}, " + f"recent_queue_size={len(recent_images_queue)}") return selected_file @@ -329,9 +354,6 @@ def random_image(request, width: Optional[int] = None, height: Optional[int] = N # Log the request details client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'unknown')) - logger.info(f"Processing image request: width={width}, height={height}, " - f"fit={fit}, client_ip={client_ip}") - # Use thread pool for image processing start_time = time.time() image_data, content_type = executor.submit( diff --git a/links/templates/links/mini_apps/image_gallery.html b/links/templates/links/mini_apps/image_gallery.html index 168acbc..2ab5589 100644 --- a/links/templates/links/mini_apps/image_gallery.html +++ b/links/templates/links/mini_apps/image_gallery.html @@ -96,7 +96,7 @@ padding: 15px 25px; display: flex; gap: 20px; - z-index: 10; + z-index: 20; box-shadow: 0 5px 25px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.1); transition: transform 0.3s ease, opacity 0.3s ease; @@ -141,6 +141,32 @@ transform: rotate(45deg); } + .next-button { + position: absolute; + bottom: 30px; + right: 120px; + width: 50px; + height: 50px; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(10px); + border-radius: 25px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 11; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; + color: white; + font-size: 16px; + } + + .next-button:hover { + transform: scale(1.1); + background: rgba(70, 70, 70, 0.8); + border-color: rgba(255, 255, 255, 0.3); + } + .control-group { display: flex; flex-direction: column; @@ -331,16 +357,19 @@ background: rgba(50, 50, 50, 0.8); color: #4ecdc4; text-decoration: none; - } - - @media (max-width: 768px) { + } @media (max-width: 768px) { .control-panel { flex-direction: column; border-radius: 20px; - bottom: 20px; + bottom: 30px; padding: 15px; } + .next-button { + right: 100px; + bottom: 30px; + } + .title { font-size: 18px; top: 20px; @@ -371,6 +400,9 @@
+
+ +
{% trans "Animation Effect" %}
@@ -384,7 +416,7 @@
{% trans "Interval (seconds)" %}
- +
@@ -398,20 +430,15 @@
{% trans "Actions" %}
-
- - -
+
- 00:15 + 00:60
@@ -426,7 +453,7 @@ // Application configuration const config = { animationType: 'fade', - interval: 15000, // 15 seconds + interval: 60000, // 60 seconds currentSlide: null, nextSlide: null, timer: null, @@ -582,9 +609,9 @@ } } - // Get random image ID (between 1000-1084) + // Get random image ID function getRandomImageId() { - return Math.floor(Math.random() * 85) + 1000; + return new Date().getTime(); } // Get image URL based on source