Enhance image swipe app mobile experience
Major improvements: - Added responsive mobile view with optimized touch interactions - Implemented image caching to preload up to 2 images for faster transitions - Made images enter consistently from left side regardless of swipe direction - Enhanced swipe animations with reduced tilt and better fade-out effects - Reduced swipe sensitivity on mobile for better tap/swipe distinction - Removed headings and reduced history button height for more screen space - Added progressive fade effect during manual swipes - Sped up slide-in animations for snappier experience - Fixed multiple edge cases for better overall stability
This commit is contained in:
281
app.py
281
app.py
@@ -7,6 +7,8 @@ import urllib.parse
|
||||
import sqlite3
|
||||
import time
|
||||
import datetime
|
||||
import zipfile
|
||||
import io
|
||||
|
||||
# Path to the image directory
|
||||
IMAGE_DIR = "/mnt/secret-items/sd-outputs/Sorted/Images/Portrait"
|
||||
@@ -14,21 +16,32 @@ IMAGE_DIR = "/mnt/secret-items/sd-outputs/Sorted/Images/Portrait"
|
||||
# Database file path
|
||||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "image_selections.db")
|
||||
|
||||
# Initialize database
|
||||
# Initialize the database
|
||||
def init_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if orientation column exists
|
||||
cursor.execute("PRAGMA table_info(image_selections)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
# Create table if it doesn't exist
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS image_selections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
image_path TEXT NOT NULL,
|
||||
resolution TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
)
|
||||
''')
|
||||
if 'image_selections' not in [table[0] for table in cursor.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()]:
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS image_selections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
image_path TEXT NOT NULL,
|
||||
resolution TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
orientation TEXT
|
||||
)
|
||||
''')
|
||||
print("Created new image_selections table with orientation column")
|
||||
elif 'orientation' not in columns:
|
||||
# Add orientation column if it doesn't exist
|
||||
cursor.execute('ALTER TABLE image_selections ADD COLUMN orientation TEXT')
|
||||
print("Added orientation column to existing table")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -36,14 +49,25 @@ def init_db():
|
||||
|
||||
# Add a selection to the database
|
||||
def add_selection(image_path, resolution, action):
|
||||
# Determine if image is portrait or landscape
|
||||
orientation = "unknown"
|
||||
try:
|
||||
from PIL import Image
|
||||
full_path = os.path.join(IMAGE_DIR, image_path.replace('/images/', ''))
|
||||
with Image.open(full_path) as img:
|
||||
width, height = img.size
|
||||
orientation = "portrait" if height > width else "landscape" if width > height else "square"
|
||||
except Exception as e:
|
||||
print(f"DEBUG ERROR determining image orientation: {str(e)}")
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Insert the selection
|
||||
cursor.execute('''
|
||||
INSERT INTO image_selections (image_path, resolution, action, timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (image_path, resolution, action, int(time.time())))
|
||||
INSERT INTO image_selections (image_path, resolution, action, timestamp, orientation)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (image_path, resolution, action, int(time.time()), orientation))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -69,6 +93,23 @@ def get_selections():
|
||||
item = {}
|
||||
for key in row.keys():
|
||||
item[key] = row[key]
|
||||
|
||||
# Ensure orientation exists
|
||||
if 'orientation' not in item or not item['orientation']:
|
||||
try:
|
||||
# Try to determine orientation if not in database
|
||||
from PIL import Image
|
||||
image_path = item['image_path']
|
||||
if image_path.startswith('/images/'):
|
||||
image_path = image_path[8:]
|
||||
full_path = os.path.join(IMAGE_DIR, image_path)
|
||||
with Image.open(full_path) as img:
|
||||
width, height = img.size
|
||||
item['orientation'] = "portrait" if height > width else "landscape" if width > height else "square"
|
||||
except Exception as e:
|
||||
print(f"DEBUG ERROR determining missing orientation: {str(e)}")
|
||||
item['orientation'] = "unknown"
|
||||
|
||||
results.append(item)
|
||||
|
||||
print(f"DEBUG: Converted {len(results)} rows to dictionaries")
|
||||
@@ -81,6 +122,25 @@ def get_selections():
|
||||
# Return empty list on error to prevent client from hanging
|
||||
return []
|
||||
|
||||
# Get a list of all image paths that have already been actioned
|
||||
def get_actioned_images():
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT DISTINCT image_path FROM image_selections
|
||||
''')
|
||||
|
||||
rows = cursor.fetchall()
|
||||
actioned_images = [row[0] for row in rows]
|
||||
|
||||
conn.close()
|
||||
return actioned_images
|
||||
except Exception as e:
|
||||
print(f"DEBUG ERROR in get_actioned_images(): {str(e)}")
|
||||
return []
|
||||
|
||||
# Update a selection in the database
|
||||
def update_selection(selection_id, action):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
@@ -144,38 +204,47 @@ class ImageSwipeHandler(BaseHTTPRequestHandler):
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
def do_GET(self):
|
||||
# Parse the URL path
|
||||
parsed_path = urllib.parse.urlparse(self.path)
|
||||
path = parsed_path.path
|
||||
# Parse the URL
|
||||
parsed_url = urllib.parse.urlparse(self.path)
|
||||
path = parsed_url.path
|
||||
|
||||
# Serve static files
|
||||
if path == "/" or path == "":
|
||||
self.serve_file("index.html")
|
||||
elif path == "/random-image":
|
||||
# Handle different paths
|
||||
if path == '/':
|
||||
self.serve_file('index.html', 'text/html')
|
||||
elif path == '/history':
|
||||
self.serve_file('history.html', 'text/html')
|
||||
elif path == '/styles.css':
|
||||
self.serve_file('styles.css', 'text/css')
|
||||
elif path == '/script.js':
|
||||
self.serve_file('script.js', 'application/javascript')
|
||||
elif path == '/random-image':
|
||||
self.serve_random_image()
|
||||
elif path == "/image-resolutions":
|
||||
self.serve_resolutions()
|
||||
elif path == "/selections":
|
||||
elif path == '/selections':
|
||||
self.serve_selections()
|
||||
elif path.startswith("/images/"):
|
||||
# Extract the image path from the URL
|
||||
image_path = path[8:] # Remove "/images/" prefix
|
||||
self.serve_image(image_path)
|
||||
elif path.startswith('/images/'):
|
||||
self.serve_image(path[8:])
|
||||
elif path.startswith('/download-selected'):
|
||||
self.handle_download_selected()
|
||||
else:
|
||||
# Serve other static files
|
||||
if path.startswith("/"):
|
||||
# Try to serve as a static file
|
||||
if path.startswith('/'):
|
||||
path = path[1:] # Remove leading slash
|
||||
self.serve_file(path)
|
||||
try:
|
||||
self.serve_file(path)
|
||||
except:
|
||||
self.send_error(404, "File not found")
|
||||
|
||||
def serve_file(self, file_path):
|
||||
def serve_file(self, file_path, content_type=None):
|
||||
try:
|
||||
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), file_path), 'rb') as file:
|
||||
content = file.read()
|
||||
|
||||
self.send_response(200)
|
||||
|
||||
# Set the content type based on file extension
|
||||
content_type, _ = mimetypes.guess_type(file_path)
|
||||
# Set the content type based on parameter or guess from file extension
|
||||
if not content_type:
|
||||
content_type, _ = mimetypes.guess_type(file_path)
|
||||
|
||||
if content_type:
|
||||
self.send_header('Content-type', content_type)
|
||||
else:
|
||||
@@ -214,23 +283,53 @@ class ImageSwipeHandler(BaseHTTPRequestHandler):
|
||||
self.send_error(404, f"Image not found: {image_path}")
|
||||
|
||||
def serve_random_image(self):
|
||||
print("DEBUG: serve_random_image() called")
|
||||
try:
|
||||
# Get list of already actioned images
|
||||
actioned_images = get_actioned_images()
|
||||
print(f"DEBUG: Found {len(actioned_images)} already actioned images")
|
||||
|
||||
# Get all resolution directories
|
||||
resolutions = [d for d in os.listdir(IMAGE_DIR) if os.path.isdir(os.path.join(IMAGE_DIR, d))]
|
||||
|
||||
# Choose a random resolution
|
||||
resolution = random.choice(resolutions)
|
||||
resolution_dir = os.path.join(IMAGE_DIR, resolution)
|
||||
|
||||
# Get all images in the selected resolution directory
|
||||
images = [f for f in os.listdir(resolution_dir) if f.endswith(('.png', '.jpg', '.jpeg'))]
|
||||
|
||||
if not images:
|
||||
self.send_error(404, "No images found in the selected resolution directory")
|
||||
return
|
||||
# Try to find an unactioned image
|
||||
max_attempts = 20 # Limit the number of attempts to find an unactioned image
|
||||
for attempt in range(max_attempts):
|
||||
# Choose a random resolution
|
||||
resolution = random.choice(resolutions)
|
||||
resolution_dir = os.path.join(IMAGE_DIR, resolution)
|
||||
|
||||
# Choose a random image
|
||||
image_name = random.choice(images)
|
||||
# Get all images in the selected resolution directory
|
||||
images = [f for f in os.listdir(resolution_dir) if f.endswith(('.png', '.jpg', '.jpeg'))]
|
||||
|
||||
if not images:
|
||||
continue # Try another resolution if this one has no images
|
||||
|
||||
# Filter out already actioned images
|
||||
unactioned_images = [img for img in images if f"{resolution}/{img}" not in actioned_images]
|
||||
|
||||
# If we have unactioned images, choose one randomly
|
||||
if unactioned_images:
|
||||
image_name = random.choice(unactioned_images)
|
||||
print(f"DEBUG: Found unactioned image: {resolution}/{image_name}")
|
||||
break
|
||||
elif attempt == max_attempts - 1:
|
||||
# If we've tried max_attempts times and still haven't found an unactioned image,
|
||||
# just choose any image
|
||||
image_name = random.choice(images)
|
||||
print(f"DEBUG: No unactioned images found after {max_attempts} attempts, using: {resolution}/{image_name}")
|
||||
else:
|
||||
# This will only execute if the for loop completes without a break
|
||||
# Choose any random image as fallback
|
||||
resolution = random.choice(resolutions)
|
||||
resolution_dir = os.path.join(IMAGE_DIR, resolution)
|
||||
images = [f for f in os.listdir(resolution_dir) if f.endswith(('.png', '.jpg', '.jpeg'))]
|
||||
if not images:
|
||||
self.send_error(404, "No images found in any resolution directory")
|
||||
return
|
||||
image_name = random.choice(images)
|
||||
print(f"DEBUG: Using fallback random image: {resolution}/{image_name}")
|
||||
|
||||
image_path = f"{resolution}/{image_name}"
|
||||
full_image_path = os.path.join(IMAGE_DIR, image_path)
|
||||
|
||||
@@ -243,12 +342,23 @@ class ImageSwipeHandler(BaseHTTPRequestHandler):
|
||||
print(f"DEBUG ERROR getting file creation time: {str(e)}")
|
||||
creation_date = "Unknown"
|
||||
|
||||
# Determine if image is portrait or landscape
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(full_image_path) as img:
|
||||
width, height = img.size
|
||||
orientation = "portrait" if height > width else "landscape" if width > height else "square"
|
||||
except Exception as e:
|
||||
print(f"DEBUG ERROR determining image orientation: {str(e)}")
|
||||
orientation = "unknown"
|
||||
|
||||
# Return the image path as JSON
|
||||
response = {
|
||||
'path': f"/images/{image_path}",
|
||||
'resolution': resolution,
|
||||
'filename': image_name,
|
||||
'creation_date': creation_date
|
||||
'creation_date': creation_date,
|
||||
'orientation': orientation
|
||||
}
|
||||
|
||||
self.send_response(200)
|
||||
@@ -322,8 +432,12 @@ class ImageSwipeHandler(BaseHTTPRequestHandler):
|
||||
self.handle_update_selection()
|
||||
elif path == "/delete-selection":
|
||||
self.handle_delete_selection()
|
||||
elif path == "/reset-database":
|
||||
elif self.path == '/reset-database':
|
||||
self.handle_reset_database()
|
||||
return
|
||||
elif self.path.startswith('/download-selected'):
|
||||
self.handle_download_selected()
|
||||
return
|
||||
else:
|
||||
self.send_error(404, "Not found")
|
||||
|
||||
@@ -443,26 +557,67 @@ class ImageSwipeHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def handle_reset_database(self):
|
||||
try:
|
||||
print("DEBUG: Handling reset database request")
|
||||
# Reset the database
|
||||
rows_affected = reset_database()
|
||||
|
||||
# Return success response
|
||||
response = {
|
||||
'success': True,
|
||||
'message': f"Database reset: {rows_affected} selections deleted"
|
||||
}
|
||||
|
||||
reset_database()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self._set_cors_headers()
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
print("DEBUG: Reset database response sent")
|
||||
self.wfile.write(json.dumps({'success': True, 'message': 'Database reset successfully'}).encode())
|
||||
except Exception as e:
|
||||
print(f"DEBUG ERROR in handle_reset_database(): {str(e)}")
|
||||
self.send_error(500, f"Error resetting database: {str(e)}")
|
||||
|
||||
print(f"DEBUG ERROR in handle_reset_database: {str(e)}")
|
||||
self.send_response(500)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'success': False, 'message': f'Error: {str(e)}'}).encode())
|
||||
|
||||
def handle_download_selected(self):
|
||||
try:
|
||||
# Parse the query parameters to get the selected image paths
|
||||
query_components = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||
image_paths = query_components.get('paths', [])
|
||||
|
||||
if not image_paths:
|
||||
self.send_response(400)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'success': False, 'message': 'No image paths provided'}).encode())
|
||||
return
|
||||
|
||||
# Create a zip file in memory
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file:
|
||||
for path in image_paths:
|
||||
# Remove the /images/ prefix
|
||||
if path.startswith('/images/'):
|
||||
path = path[8:]
|
||||
|
||||
full_path = os.path.join(IMAGE_DIR, path)
|
||||
if os.path.exists(full_path):
|
||||
# Add the file to the zip with just the filename (no directory structure)
|
||||
filename = os.path.basename(path)
|
||||
zip_file.write(full_path, filename)
|
||||
|
||||
# Seek to the beginning of the buffer
|
||||
zip_buffer.seek(0)
|
||||
|
||||
# Send the zip file as a response
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/zip')
|
||||
self.send_header('Content-Disposition', 'attachment; filename="selected_images.zip"')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(zip_buffer.getvalue())
|
||||
|
||||
except Exception as e:
|
||||
print(f"DEBUG ERROR in handle_download_selected: {str(e)}")
|
||||
self.send_response(500)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'success': False, 'message': f'Error: {str(e)}'}).encode())
|
||||
|
||||
def do_OPTIONS(self):
|
||||
# Handle preflight requests for CORS
|
||||
self.send_response(200)
|
||||
|
||||
Reference in New Issue
Block a user