Initial commit: Image Swipe App with SQLite database
This commit is contained in:
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# SQLite database
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# OS specific files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Image Swipe App
|
||||||
|
|
||||||
|
A web application for sorting and organizing images using swipe gestures, similar to dating apps.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Swipe Interface**: Swipe images in four directions (left, right, up, down) to categorize them
|
||||||
|
- **Full-size Image View**: Click on any image to view it in full resolution with metadata
|
||||||
|
- **History Page**: View all your past selections with filtering options
|
||||||
|
- **Database Storage**: All selections are saved in a SQLite database
|
||||||
|
- **Reset Functionality**: Option to clear all selections and start fresh
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- `app.py`: Python server that handles API requests and serves files
|
||||||
|
- `index.html`: Main page with the swipe interface
|
||||||
|
- `history.html`: Page to view and manage past selections
|
||||||
|
- `script.js`: JavaScript for the swipe functionality and UI interactions
|
||||||
|
- `styles.css`: CSS styling for the application
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
1. Run the server: `python app.py`
|
||||||
|
2. Open a web browser and navigate to `http://localhost:8000`
|
||||||
|
3. Swipe images or use the buttons to categorize them:
|
||||||
|
- Left: Discard
|
||||||
|
- Right: Keep
|
||||||
|
- Up: Favorite
|
||||||
|
- Down: Review Later
|
||||||
|
4. Click on an image to view it in full resolution
|
||||||
|
5. Use the "View History" link to see all your selections
|
||||||
|
6. Use the "Reset Database" button in the history page to clear all selections
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- Standard Python libraries (http.server, sqlite3, etc.)
|
||||||
|
- Web browser with JavaScript enabled
|
||||||
484
app.py
Normal file
484
app.py
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import mimetypes
|
||||||
|
import urllib.parse
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# Path to the image directory
|
||||||
|
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
|
||||||
|
def init_db():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print(f"Database initialized at {DB_PATH}")
|
||||||
|
|
||||||
|
# Add a selection to the database
|
||||||
|
def add_selection(image_path, resolution, action):
|
||||||
|
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())))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Get all selections from the database
|
||||||
|
def get_selections():
|
||||||
|
print("DEBUG: get_selections() called")
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row # This enables column access by name
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM image_selections ORDER BY timestamp DESC
|
||||||
|
''')
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
print(f"DEBUG: Fetched {len(rows)} rows from database")
|
||||||
|
|
||||||
|
# Properly convert SQLite Row objects to dictionaries
|
||||||
|
results = []
|
||||||
|
for row in rows:
|
||||||
|
item = {}
|
||||||
|
for key in row.keys():
|
||||||
|
item[key] = row[key]
|
||||||
|
results.append(item)
|
||||||
|
|
||||||
|
print(f"DEBUG: Converted {len(results)} rows to dictionaries")
|
||||||
|
print(f"DEBUG: First result (if any): {results[0] if results else 'None'}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG ERROR in get_selections(): {str(e)}")
|
||||||
|
# Return empty list on error to prevent client from hanging
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Update a selection in the database
|
||||||
|
def update_selection(selection_id, action):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Update the selection
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE image_selections SET action = ?, timestamp = ? WHERE id = ?
|
||||||
|
''', (action, int(time.time()), selection_id))
|
||||||
|
|
||||||
|
# Check if a row was affected
|
||||||
|
rows_affected = cursor.rowcount
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return rows_affected > 0
|
||||||
|
|
||||||
|
# Delete a selection from the database
|
||||||
|
def delete_selection(selection_id):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Delete the selection
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM image_selections WHERE id = ?
|
||||||
|
''', (selection_id,))
|
||||||
|
|
||||||
|
# Check if a row was affected
|
||||||
|
rows_affected = cursor.rowcount
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return rows_affected > 0
|
||||||
|
|
||||||
|
# Reset the database by deleting all selections
|
||||||
|
def reset_database():
|
||||||
|
print("DEBUG: Resetting database - deleting all selections")
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Delete all selections
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM image_selections
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Get the number of rows affected
|
||||||
|
rows_affected = cursor.rowcount
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"DEBUG: Reset database - deleted {rows_affected} selections")
|
||||||
|
return rows_affected
|
||||||
|
|
||||||
|
class ImageSwipeHandler(BaseHTTPRequestHandler):
|
||||||
|
# Set response headers for CORS
|
||||||
|
def _set_cors_headers(self):
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
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
|
||||||
|
|
||||||
|
# Serve static files
|
||||||
|
if path == "/" or path == "":
|
||||||
|
self.serve_file("index.html")
|
||||||
|
elif path == "/random-image":
|
||||||
|
self.serve_random_image()
|
||||||
|
elif path == "/image-resolutions":
|
||||||
|
self.serve_resolutions()
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
# Serve other static files
|
||||||
|
if path.startswith("/"):
|
||||||
|
path = path[1:] # Remove leading slash
|
||||||
|
self.serve_file(path)
|
||||||
|
|
||||||
|
def serve_file(self, file_path):
|
||||||
|
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)
|
||||||
|
if content_type:
|
||||||
|
self.send_header('Content-type', content_type)
|
||||||
|
else:
|
||||||
|
self.send_header('Content-type', 'application/octet-stream')
|
||||||
|
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.send_header('Content-length', len(content))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(content)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.send_error(404, f"File not found: {file_path}")
|
||||||
|
|
||||||
|
def serve_image(self, image_path):
|
||||||
|
try:
|
||||||
|
# Decode URL-encoded path
|
||||||
|
image_path = urllib.parse.unquote(image_path)
|
||||||
|
full_path = os.path.join(IMAGE_DIR, image_path)
|
||||||
|
|
||||||
|
with open(full_path, 'rb') as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
|
||||||
|
# Set the content type based on file extension
|
||||||
|
content_type, _ = mimetypes.guess_type(full_path)
|
||||||
|
if content_type:
|
||||||
|
self.send_header('Content-type', content_type)
|
||||||
|
else:
|
||||||
|
self.send_header('Content-type', 'application/octet-stream')
|
||||||
|
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.send_header('Content-length', len(content))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(content)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.send_error(404, f"Image not found: {image_path}")
|
||||||
|
|
||||||
|
def serve_random_image(self):
|
||||||
|
try:
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Choose a random image
|
||||||
|
image_name = random.choice(images)
|
||||||
|
image_path = f"{resolution}/{image_name}"
|
||||||
|
full_image_path = os.path.join(IMAGE_DIR, image_path)
|
||||||
|
|
||||||
|
# Get the file creation time
|
||||||
|
try:
|
||||||
|
file_stat = os.stat(full_image_path)
|
||||||
|
creation_time = file_stat.st_mtime # Use modification time as creation time
|
||||||
|
creation_date = datetime.datetime.fromtimestamp(creation_time).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG ERROR getting file creation time: {str(e)}")
|
||||||
|
creation_date = "Unknown"
|
||||||
|
|
||||||
|
# Return the image path as JSON
|
||||||
|
response = {
|
||||||
|
'path': f"/images/{image_path}",
|
||||||
|
'resolution': resolution,
|
||||||
|
'filename': image_name,
|
||||||
|
'creation_date': creation_date
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(500, f"Error serving random image: {str(e)}")
|
||||||
|
|
||||||
|
def serve_resolutions(self):
|
||||||
|
try:
|
||||||
|
# Get all resolution directories
|
||||||
|
resolutions = [d for d in os.listdir(IMAGE_DIR) if os.path.isdir(os.path.join(IMAGE_DIR, d))]
|
||||||
|
|
||||||
|
# Return the resolutions as JSON
|
||||||
|
response = {
|
||||||
|
'resolutions': resolutions
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(500, f"Error serving resolutions: {str(e)}")
|
||||||
|
|
||||||
|
def serve_selections(self):
|
||||||
|
print("DEBUG: serve_selections() called")
|
||||||
|
try:
|
||||||
|
# Get all selections from the database
|
||||||
|
selections = get_selections()
|
||||||
|
|
||||||
|
# Return the selections as JSON
|
||||||
|
response = {
|
||||||
|
'selections': selections
|
||||||
|
}
|
||||||
|
|
||||||
|
# Debug the response before sending
|
||||||
|
print(f"DEBUG: Response has {len(selections)} selections")
|
||||||
|
|
||||||
|
# Try to serialize to JSON to catch any serialization errors
|
||||||
|
try:
|
||||||
|
response_json = json.dumps(response)
|
||||||
|
print(f"DEBUG: JSON serialization successful, length: {len(response_json)}")
|
||||||
|
except Exception as json_err:
|
||||||
|
print(f"DEBUG ERROR in JSON serialization: {str(json_err)}")
|
||||||
|
# If there's an error in serialization, send a simpler response
|
||||||
|
response = {'selections': [], 'error': 'JSON serialization error'}
|
||||||
|
response_json = json.dumps(response)
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(response_json.encode())
|
||||||
|
print("DEBUG: Response sent successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG ERROR in serve_selections(): {str(e)}")
|
||||||
|
self.send_error(500, f"Error serving selections: {str(e)}")
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
# Parse the URL path
|
||||||
|
parsed_path = urllib.parse.urlparse(self.path)
|
||||||
|
path = parsed_path.path
|
||||||
|
|
||||||
|
if path == "/record-selection":
|
||||||
|
self.handle_record_selection()
|
||||||
|
elif path == "/update-selection":
|
||||||
|
self.handle_update_selection()
|
||||||
|
elif path == "/delete-selection":
|
||||||
|
self.handle_delete_selection()
|
||||||
|
elif path == "/reset-database":
|
||||||
|
self.handle_reset_database()
|
||||||
|
else:
|
||||||
|
self.send_error(404, "Not found")
|
||||||
|
|
||||||
|
def handle_record_selection(self):
|
||||||
|
try:
|
||||||
|
# Get the content length
|
||||||
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
|
||||||
|
# Read the request body
|
||||||
|
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||||
|
data = json.loads(post_data)
|
||||||
|
|
||||||
|
# Extract the required fields
|
||||||
|
image_path = data.get('path', '').replace('/images/', '')
|
||||||
|
resolution = data.get('resolution', '')
|
||||||
|
action = data.get('action', '')
|
||||||
|
|
||||||
|
# Validate the data
|
||||||
|
if not image_path or not resolution or not action:
|
||||||
|
self.send_error(400, "Missing required fields")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add the selection to the database
|
||||||
|
add_selection(image_path, resolution, action)
|
||||||
|
|
||||||
|
# Return success response
|
||||||
|
response = {
|
||||||
|
'success': True,
|
||||||
|
'message': f"Selection recorded: {action} for {image_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(500, f"Error recording selection: {str(e)}")
|
||||||
|
|
||||||
|
def handle_update_selection(self):
|
||||||
|
try:
|
||||||
|
# Get the content length
|
||||||
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
|
||||||
|
# Read the request body
|
||||||
|
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||||
|
data = json.loads(post_data)
|
||||||
|
|
||||||
|
# Extract the required fields
|
||||||
|
selection_id = data.get('id')
|
||||||
|
action = data.get('action', '')
|
||||||
|
|
||||||
|
# Validate the data
|
||||||
|
if not selection_id or not action:
|
||||||
|
self.send_error(400, "Missing required fields")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update the selection in the database
|
||||||
|
success = update_selection(selection_id, action)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
self.send_error(404, f"Selection with ID {selection_id} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Return success response
|
||||||
|
response = {
|
||||||
|
'success': True,
|
||||||
|
'message': f"Selection updated: ID {selection_id} to {action}"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(500, f"Error updating selection: {str(e)}")
|
||||||
|
|
||||||
|
def handle_delete_selection(self):
|
||||||
|
try:
|
||||||
|
# Get the content length
|
||||||
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
|
||||||
|
# Read the request body
|
||||||
|
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||||
|
data = json.loads(post_data)
|
||||||
|
|
||||||
|
# Extract the required fields
|
||||||
|
selection_id = data.get('id')
|
||||||
|
|
||||||
|
# Validate the data
|
||||||
|
if not selection_id:
|
||||||
|
self.send_error(400, "Missing selection ID")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delete the selection from the database
|
||||||
|
success = delete_selection(selection_id)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
self.send_error(404, f"Selection with ID {selection_id} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Return success response
|
||||||
|
response = {
|
||||||
|
'success': True,
|
||||||
|
'message': f"Selection deleted: ID {selection_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG ERROR in handle_delete_selection(): {str(e)}")
|
||||||
|
self.send_error(500, f"Error deleting selection: {str(e)}")
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(response).encode())
|
||||||
|
print("DEBUG: Reset database response sent")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG ERROR in handle_reset_database(): {str(e)}")
|
||||||
|
self.send_error(500, f"Error resetting database: {str(e)}")
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
# Handle preflight requests for CORS
|
||||||
|
self.send_response(200)
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def run(server_class=HTTPServer, handler_class=ImageSwipeHandler, port=8000):
|
||||||
|
# Initialize the database
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
server_address = ('', port)
|
||||||
|
httpd = server_class(server_address, handler_class)
|
||||||
|
print(f"Starting server on port {port}...")
|
||||||
|
print(f"Image directory: {IMAGE_DIR}")
|
||||||
|
print(f"Database: {DB_PATH}")
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
628
history.html
Normal file
628
history.html
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Image Selection History</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.history-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #ff4757;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button:hover {
|
||||||
|
background-color: #ff6b81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #ff4757;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #ddd;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-item {
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
background-color: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-info {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-action {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 8px 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-item:hover .selection-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
color: #1e90ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-left {
|
||||||
|
background-color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-right {
|
||||||
|
background-color: #2ed573;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-up {
|
||||||
|
background-color: #1e90ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-down {
|
||||||
|
background-color: #ffa502;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button.active {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-all {
|
||||||
|
background-color: #ddd;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-left {
|
||||||
|
background-color: #ff4757;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-right {
|
||||||
|
background-color: #2ed573;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-up {
|
||||||
|
background-color: #1e90ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-down {
|
||||||
|
background-color: #ffa502;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-selections {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: #777;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Action change modal -->
|
||||||
|
<div id="action-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content" style="max-width: 400px; height: auto;">
|
||||||
|
<span class="close-modal" id="close-action-modal">×</span>
|
||||||
|
<h2>Change Action</h2>
|
||||||
|
<div id="modal-image-preview" style="margin: 15px 0; text-align: center;">
|
||||||
|
<img id="modal-preview-img" src="" alt="Image preview" style="max-height: 200px; max-width: 100%;">
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons" style="margin: 20px 0;">
|
||||||
|
<button class="action-btn" data-action="left" style="background-color: #ff4757;">Discard</button>
|
||||||
|
<button class="action-btn" data-action="right" style="background-color: #2ed573;">Keep</button>
|
||||||
|
<button class="action-btn" data-action="up" style="background-color: #1e90ff;">Favorite</button>
|
||||||
|
<button class="action-btn" data-action="down" style="background-color: #ffa502;">Review</button>
|
||||||
|
</div>
|
||||||
|
<div id="modal-message" style="text-align: center; margin-top: 10px; color: #666;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-container">
|
||||||
|
<div class="history-header">
|
||||||
|
<h1>Image Selection History</h1>
|
||||||
|
<div class="header-buttons">
|
||||||
|
<button id="reset-button" class="reset-button">Reset Database</button>
|
||||||
|
<a href="/" class="back-button">Back to Swiper</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reset confirmation modal -->
|
||||||
|
<div id="reset-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content" style="max-width: 400px; height: auto;">
|
||||||
|
<h2>Reset Database</h2>
|
||||||
|
<p>Are you sure you want to delete all selections? This cannot be undone.</p>
|
||||||
|
<div class="reset-modal-buttons">
|
||||||
|
<button id="confirm-reset" class="danger-button">Yes, Delete All</button>
|
||||||
|
<button id="cancel-reset" class="cancel-button">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<div id="reset-message" style="text-align: center; margin-top: 15px; color: #666;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-controls">
|
||||||
|
<button class="filter-button filter-all active" data-filter="all">All</button>
|
||||||
|
<button class="filter-button filter-left" data-filter="left">Discarded</button>
|
||||||
|
<button class="filter-button filter-right" data-filter="right">Kept</button>
|
||||||
|
<button class="filter-button filter-up" data-filter="up">Favorited</button>
|
||||||
|
<button class="filter-button filter-down" data-filter="down">Review Later</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="selection-grid" class="selection-grid">
|
||||||
|
<!-- Selection items will be loaded here dynamically -->
|
||||||
|
<div class="no-selections">Loading selections...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const selectionGrid = document.getElementById('selection-grid');
|
||||||
|
const filterButtons = document.querySelectorAll('.filter-button');
|
||||||
|
let currentFilter = 'all';
|
||||||
|
let allSelections = [];
|
||||||
|
|
||||||
|
// Load selections from the server
|
||||||
|
loadSelections();
|
||||||
|
|
||||||
|
// Add event listeners to filter buttons
|
||||||
|
filterButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
// Update active button
|
||||||
|
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
|
||||||
|
// Apply filter
|
||||||
|
currentFilter = this.dataset.filter;
|
||||||
|
renderSelections();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadSelections() {
|
||||||
|
console.log('DEBUG: loadSelections() called');
|
||||||
|
selectionGrid.innerHTML = `<div class="no-selections">Loading selections...</div>`;
|
||||||
|
|
||||||
|
fetch('/selections')
|
||||||
|
.then(response => {
|
||||||
|
console.log('DEBUG: Fetch response received:', response.status, response.statusText);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch selections: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.text().then(text => {
|
||||||
|
console.log('DEBUG: Raw response text:', text.substring(0, 200) + '...');
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('DEBUG: JSON parse error:', e);
|
||||||
|
throw new Error(`Invalid JSON response: ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('DEBUG: Data received:', data);
|
||||||
|
console.log('DEBUG: Selections count:', data.selections ? data.selections.length : 'undefined');
|
||||||
|
|
||||||
|
if (!data.selections) {
|
||||||
|
throw new Error('No selections array in response');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix: Ensure each selection has an id property as a number
|
||||||
|
allSelections = data.selections.map(selection => {
|
||||||
|
// Make sure id is a number
|
||||||
|
if (selection.id) {
|
||||||
|
selection.id = parseInt(selection.id);
|
||||||
|
}
|
||||||
|
return selection;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('DEBUG: First selection (if any):', allSelections.length > 0 ? allSelections[0] : 'none');
|
||||||
|
renderSelections();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('DEBUG ERROR in loadSelections():', error);
|
||||||
|
selectionGrid.innerHTML = `
|
||||||
|
<div class="no-selections">
|
||||||
|
Error loading selections: ${error.message}<br>
|
||||||
|
<button onclick="loadSelections()" style="margin-top: 10px;">Try Again</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables for the action change modal
|
||||||
|
const actionModal = document.getElementById('action-modal');
|
||||||
|
const closeActionModal = document.getElementById('close-action-modal');
|
||||||
|
const modalPreviewImg = document.getElementById('modal-preview-img');
|
||||||
|
const modalMessage = document.getElementById('modal-message');
|
||||||
|
const actionButtons = actionModal.querySelectorAll('.action-btn');
|
||||||
|
let currentSelectionId = null;
|
||||||
|
|
||||||
|
// Variables for the reset modal
|
||||||
|
const resetButton = document.getElementById('reset-button');
|
||||||
|
const resetModal = document.getElementById('reset-modal');
|
||||||
|
const confirmResetButton = document.getElementById('confirm-reset');
|
||||||
|
const cancelResetButton = document.getElementById('cancel-reset');
|
||||||
|
const resetMessage = document.getElementById('reset-message');
|
||||||
|
|
||||||
|
// Close the action modal when clicking the close button
|
||||||
|
closeActionModal.addEventListener('click', function() {
|
||||||
|
actionModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the action modal when clicking outside the content
|
||||||
|
window.addEventListener('click', function(e) {
|
||||||
|
if (e.target === actionModal) {
|
||||||
|
actionModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (e.target === resetModal) {
|
||||||
|
resetModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset button click handler
|
||||||
|
resetButton.addEventListener('click', function() {
|
||||||
|
resetModal.style.display = 'block';
|
||||||
|
resetMessage.textContent = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel reset button click handler
|
||||||
|
cancelResetButton.addEventListener('click', function() {
|
||||||
|
resetModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm reset button click handler
|
||||||
|
confirmResetButton.addEventListener('click', function() {
|
||||||
|
resetDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to reset the database
|
||||||
|
function resetDatabase() {
|
||||||
|
resetMessage.textContent = 'Resetting database...';
|
||||||
|
confirmResetButton.disabled = true;
|
||||||
|
cancelResetButton.disabled = true;
|
||||||
|
|
||||||
|
fetch('/reset-database', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to reset database');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
resetMessage.textContent = data.message || 'Database reset successfully!';
|
||||||
|
|
||||||
|
// Clear the selections array
|
||||||
|
allSelections = [];
|
||||||
|
|
||||||
|
// Re-render the selections
|
||||||
|
renderSelections();
|
||||||
|
|
||||||
|
// Re-enable buttons after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
confirmResetButton.disabled = false;
|
||||||
|
cancelResetButton.disabled = false;
|
||||||
|
|
||||||
|
// Close the modal after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
resetModal.style.display = 'none';
|
||||||
|
}, 1000);
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error resetting database:', error);
|
||||||
|
resetMessage.textContent = `Error: ${error.message}`;
|
||||||
|
confirmResetButton.disabled = false;
|
||||||
|
cancelResetButton.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle action button clicks in the modal
|
||||||
|
actionButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const action = this.dataset.action;
|
||||||
|
updateSelectionAction(currentSelectionId, action);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to update a selection's action
|
||||||
|
function updateSelectionAction(id, action) {
|
||||||
|
modalMessage.textContent = 'Updating...';
|
||||||
|
|
||||||
|
fetch('/update-selection', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: id,
|
||||||
|
action: action
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update selection');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
modalMessage.textContent = 'Updated successfully!';
|
||||||
|
|
||||||
|
// Update the selection in our local data
|
||||||
|
const selection = allSelections.find(s => s.id === id);
|
||||||
|
if (selection) {
|
||||||
|
selection.action = action;
|
||||||
|
selection.timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render the selections
|
||||||
|
renderSelections();
|
||||||
|
|
||||||
|
// Close the modal after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
actionModal.style.display = 'none';
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error updating selection:', error);
|
||||||
|
modalMessage.textContent = 'Error updating selection';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to delete a selection
|
||||||
|
function deleteSelection(id) {
|
||||||
|
if (!confirm('Are you sure you want to delete this selection?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/delete-selection', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete selection');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Remove the selection from our local data
|
||||||
|
allSelections = allSelections.filter(s => s.id !== id);
|
||||||
|
|
||||||
|
// Re-render the selections
|
||||||
|
renderSelections();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting selection:', error);
|
||||||
|
alert('Error deleting selection');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelections() {
|
||||||
|
console.log('DEBUG: renderSelections called with filter:', currentFilter);
|
||||||
|
console.log('DEBUG: Total selections:', allSelections.length);
|
||||||
|
|
||||||
|
// Filter selections based on current filter
|
||||||
|
const filteredSelections = currentFilter === 'all'
|
||||||
|
? allSelections
|
||||||
|
: allSelections.filter(s => s.action === currentFilter);
|
||||||
|
|
||||||
|
console.log('DEBUG: Filtered selections count:', filteredSelections.length);
|
||||||
|
|
||||||
|
// Clear the grid
|
||||||
|
selectionGrid.innerHTML = '';
|
||||||
|
|
||||||
|
// Show message if no selections
|
||||||
|
if (filteredSelections.length === 0) {
|
||||||
|
selectionGrid.innerHTML = `
|
||||||
|
<div class="no-selections">
|
||||||
|
No ${currentFilter === 'all' ? '' : currentFilter + ' '} selections found.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render each selection
|
||||||
|
filteredSelections.forEach(selection => {
|
||||||
|
const date = new Date(selection.timestamp * 1000);
|
||||||
|
const formattedDate = date.toLocaleString();
|
||||||
|
|
||||||
|
const selectionItem = document.createElement('div');
|
||||||
|
selectionItem.className = 'selection-item';
|
||||||
|
selectionItem.innerHTML = `
|
||||||
|
<div class="selection-action action-${selection.action}">${getActionName(selection.action)}</div>
|
||||||
|
<img src="/images/${selection.image_path}" alt="Selected image">
|
||||||
|
<div class="selection-info">
|
||||||
|
<div>Resolution: ${selection.resolution}</div>
|
||||||
|
<div class="timestamp">${formattedDate}</div>
|
||||||
|
</div>
|
||||||
|
<div class="selection-controls">
|
||||||
|
<button class="control-btn edit-btn" data-id="${selection.id}">Change Action</button>
|
||||||
|
<button class="control-btn delete-btn" data-id="${selection.id}">Remove</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add click event to view full image
|
||||||
|
selectionItem.querySelector('img').addEventListener('click', function() {
|
||||||
|
window.open(`/images/${selection.image_path}`, '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners for the control buttons
|
||||||
|
const editBtn = selectionItem.querySelector('.edit-btn');
|
||||||
|
const deleteBtn = selectionItem.querySelector('.delete-btn');
|
||||||
|
|
||||||
|
editBtn.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = parseInt(this.dataset.id);
|
||||||
|
openActionModal(selection);
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteBtn.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = parseInt(this.dataset.id);
|
||||||
|
deleteSelection(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
selectionGrid.appendChild(selectionItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to open the action change modal
|
||||||
|
function openActionModal(selection) {
|
||||||
|
console.log('DEBUG: Opening action modal for selection:', selection);
|
||||||
|
currentSelectionId = selection.id;
|
||||||
|
modalPreviewImg.src = `/images/${selection.image_path}`;
|
||||||
|
modalMessage.textContent = '';
|
||||||
|
actionModal.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionName(action) {
|
||||||
|
switch(action) {
|
||||||
|
case 'left': return 'Discard';
|
||||||
|
case 'right': return 'Keep';
|
||||||
|
case 'up': return 'Favorite';
|
||||||
|
case 'down': return 'Review';
|
||||||
|
default: return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
66
index.html
Normal file
66
index.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Image Swipe App</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Image Swiper</h1>
|
||||||
|
<p class="subtitle">Swipe to sort your images</p>
|
||||||
|
<a href="/history.html" class="history-link">View History</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="swipe-container">
|
||||||
|
<div class="image-card" id="current-card">
|
||||||
|
<!-- Image will be loaded here dynamically -->
|
||||||
|
<img src="data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3ELoading...%3C%2Ftext%3E%3C%2Fsvg%3E" alt="Image">
|
||||||
|
<div class="loading-indicator">Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="swipe-actions">
|
||||||
|
<div class="action-hint left-hint">← Swipe Left</div>
|
||||||
|
<div class="action-hint right-hint">Swipe Right →</div>
|
||||||
|
<div class="action-hint up-hint">↑ Swipe Up</div>
|
||||||
|
<div class="action-hint down-hint">Swipe Down ↓</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button id="btn-left" class="action-btn">Left</button>
|
||||||
|
<button id="btn-right" class="action-btn">Right</button>
|
||||||
|
<button id="btn-up" class="action-btn">Up</button>
|
||||||
|
<button id="btn-down" class="action-btn">Down</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-area">
|
||||||
|
<p>Current resolution: Loading...</p>
|
||||||
|
<p id="last-action">Last action: None</p>
|
||||||
|
<div class="swipe-legend">
|
||||||
|
<div class="legend-item"><span class="legend-color left-color"></span> Left: Discard</div>
|
||||||
|
<div class="legend-item"><span class="legend-color right-color"></span> Right: Keep</div>
|
||||||
|
<div class="legend-item"><span class="legend-color up-color"></span> Up: Favorite</div>
|
||||||
|
<div class="legend-item"><span class="legend-color down-color"></span> Down: Review Later</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Full-resolution image modal -->
|
||||||
|
<div id="fullscreen-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close-modal">×</span>
|
||||||
|
<img id="fullscreen-image" src="" alt="Full resolution image">
|
||||||
|
<div class="modal-info">
|
||||||
|
<p id="modal-resolution">Resolution: </p>
|
||||||
|
<p id="modal-filename">Filename: </p>
|
||||||
|
<p id="modal-creation-date">Creation Date: </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
344
script.js
Normal file
344
script.js
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const card = document.getElementById('current-card');
|
||||||
|
const lastActionText = document.getElementById('last-action');
|
||||||
|
const leftHint = document.querySelector('.left-hint');
|
||||||
|
const rightHint = document.querySelector('.right-hint');
|
||||||
|
const upHint = document.querySelector('.up-hint');
|
||||||
|
const downHint = document.querySelector('.down-hint');
|
||||||
|
|
||||||
|
// Modal elements
|
||||||
|
const modal = document.getElementById('fullscreen-modal');
|
||||||
|
const fullscreenImage = document.getElementById('fullscreen-image');
|
||||||
|
const closeModal = document.querySelector('.close-modal');
|
||||||
|
const modalResolution = document.getElementById('modal-resolution');
|
||||||
|
const modalFilename = document.getElementById('modal-filename');
|
||||||
|
const modalCreationDate = document.getElementById('modal-creation-date');
|
||||||
|
|
||||||
|
// Current image information
|
||||||
|
let currentImageInfo = null;
|
||||||
|
|
||||||
|
// Button event listeners
|
||||||
|
document.getElementById('btn-left').addEventListener('click', () => performSwipe('left'));
|
||||||
|
document.getElementById('btn-right').addEventListener('click', () => performSwipe('right'));
|
||||||
|
document.getElementById('btn-up').addEventListener('click', () => performSwipe('up'));
|
||||||
|
document.getElementById('btn-down').addEventListener('click', () => performSwipe('down'));
|
||||||
|
|
||||||
|
// Touch start time for distinguishing between swipe and tap
|
||||||
|
let touchStartTime = 0;
|
||||||
|
|
||||||
|
// Touch variables
|
||||||
|
let startX, startY, moveX, moveY;
|
||||||
|
let isDragging = false;
|
||||||
|
const swipeThreshold = 100; // Minimum distance for a swipe to be registered
|
||||||
|
|
||||||
|
// Touch event handlers
|
||||||
|
card.addEventListener('touchstart', handleTouchStart, false);
|
||||||
|
card.addEventListener('touchmove', handleTouchMove, false);
|
||||||
|
card.addEventListener('touchend', handleTouchEnd, false);
|
||||||
|
|
||||||
|
// Mouse event handlers (for desktop testing)
|
||||||
|
card.addEventListener('mousedown', handleMouseDown, false);
|
||||||
|
document.addEventListener('mousemove', handleMouseMove, false);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp, false);
|
||||||
|
|
||||||
|
// Click handler for viewing full-resolution image
|
||||||
|
card.addEventListener('click', handleCardClick);
|
||||||
|
|
||||||
|
// Close modal when clicking the close button
|
||||||
|
closeModal.addEventListener('click', () => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside the image
|
||||||
|
window.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal with escape key
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && modal.style.display === 'block') {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleTouchStart(e) {
|
||||||
|
startX = e.touches[0].clientX;
|
||||||
|
startY = e.touches[0].clientY;
|
||||||
|
isDragging = true;
|
||||||
|
card.classList.add('swiping');
|
||||||
|
|
||||||
|
// Store touch start time to differentiate between swipe and tap
|
||||||
|
touchStartTime = new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchMove(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
moveX = e.touches[0].clientX - startX;
|
||||||
|
moveY = e.touches[0].clientY - startY;
|
||||||
|
|
||||||
|
// Apply transform to the card
|
||||||
|
card.style.transform = `translate(${moveX}px, ${moveY}px) rotate(${moveX * 0.1}deg)`;
|
||||||
|
|
||||||
|
// Show appropriate hint based on direction
|
||||||
|
updateHints(moveX, moveY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchEnd(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
// Calculate touch duration
|
||||||
|
const touchEndTime = new Date().getTime();
|
||||||
|
const touchDuration = touchEndTime - touchStartTime;
|
||||||
|
|
||||||
|
// Determine if this was a tap (short touch with minimal movement)
|
||||||
|
const absX = Math.abs(moveX || 0);
|
||||||
|
const absY = Math.abs(moveY || 0);
|
||||||
|
const isTap = touchDuration < 300 && Math.max(absX, absY) < 10;
|
||||||
|
|
||||||
|
isDragging = false;
|
||||||
|
|
||||||
|
if (isTap) {
|
||||||
|
// This was a tap, not a swipe
|
||||||
|
resetCardPosition();
|
||||||
|
handleCardClick(e);
|
||||||
|
} else if (Math.max(absX, absY) > swipeThreshold) {
|
||||||
|
// This was a swipe
|
||||||
|
if (absX > absY) {
|
||||||
|
// Horizontal swipe
|
||||||
|
if (moveX > 0) {
|
||||||
|
performSwipe('right');
|
||||||
|
} else {
|
||||||
|
performSwipe('left');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Vertical swipe
|
||||||
|
if (moveY > 0) {
|
||||||
|
performSwipe('down');
|
||||||
|
} else {
|
||||||
|
performSwipe('up');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset card position if swipe wasn't strong enough
|
||||||
|
resetCardPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide all hints
|
||||||
|
hideAllHints();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(e) {
|
||||||
|
// Store the initial position and set the dragging flag
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
isDragging = true;
|
||||||
|
card.classList.add('swiping');
|
||||||
|
|
||||||
|
// Prevent default to avoid text selection during drag
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
moveX = e.clientX - startX;
|
||||||
|
moveY = e.clientY - startY;
|
||||||
|
|
||||||
|
// Apply transform to the card
|
||||||
|
card.style.transform = `translate(${moveX}px, ${moveY}px) rotate(${moveX * 0.1}deg)`;
|
||||||
|
|
||||||
|
// Show appropriate hint based on direction
|
||||||
|
updateHints(moveX, moveY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
// Determine if this was a click (minimal movement) or a swipe
|
||||||
|
const absX = Math.abs(moveX || 0);
|
||||||
|
const absY = Math.abs(moveY || 0);
|
||||||
|
|
||||||
|
isDragging = false;
|
||||||
|
|
||||||
|
if (Math.max(absX, absY) > swipeThreshold) {
|
||||||
|
if (absX > absY) {
|
||||||
|
// Horizontal swipe
|
||||||
|
if (moveX > 0) {
|
||||||
|
performSwipe('right');
|
||||||
|
} else {
|
||||||
|
performSwipe('left');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Vertical swipe
|
||||||
|
if (moveY > 0) {
|
||||||
|
performSwipe('down');
|
||||||
|
} else {
|
||||||
|
performSwipe('up');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset card position if swipe wasn't strong enough
|
||||||
|
resetCardPosition();
|
||||||
|
// We don't trigger click here because the card already has a click event listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide all hints
|
||||||
|
hideAllHints();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHints(moveX, moveY) {
|
||||||
|
hideAllHints();
|
||||||
|
|
||||||
|
const absX = Math.abs(moveX);
|
||||||
|
const absY = Math.abs(moveY);
|
||||||
|
|
||||||
|
if (absX > absY) {
|
||||||
|
// Horizontal movement is dominant
|
||||||
|
if (moveX > 0) {
|
||||||
|
rightHint.style.opacity = '1';
|
||||||
|
} else {
|
||||||
|
leftHint.style.opacity = '1';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Vertical movement is dominant
|
||||||
|
if (moveY > 0) {
|
||||||
|
downHint.style.opacity = '1';
|
||||||
|
} else {
|
||||||
|
upHint.style.opacity = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAllHints() {
|
||||||
|
leftHint.style.opacity = '0';
|
||||||
|
rightHint.style.opacity = '0';
|
||||||
|
upHint.style.opacity = '0';
|
||||||
|
downHint.style.opacity = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCardPosition() {
|
||||||
|
card.classList.remove('swiping');
|
||||||
|
card.style.transform = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function performSwipe(direction) {
|
||||||
|
// Add the appropriate swipe class
|
||||||
|
card.classList.add(`swipe-${direction}`);
|
||||||
|
|
||||||
|
// Update the last action text
|
||||||
|
lastActionText.textContent = `Last action: Swiped ${direction}`;
|
||||||
|
|
||||||
|
// Record the selection in the database if we have a current image
|
||||||
|
if (currentImageInfo) {
|
||||||
|
recordSelection(currentImageInfo, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After animation completes, reset and load a new image
|
||||||
|
setTimeout(() => {
|
||||||
|
card.classList.remove(`swipe-${direction}`);
|
||||||
|
card.classList.remove('swiping');
|
||||||
|
card.style.transform = '';
|
||||||
|
|
||||||
|
// Load a new random image from our server
|
||||||
|
loadNewImage();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to record a selection in the database
|
||||||
|
function recordSelection(imageInfo, action) {
|
||||||
|
// Create the data to send
|
||||||
|
const data = {
|
||||||
|
path: imageInfo.path,
|
||||||
|
resolution: imageInfo.resolution,
|
||||||
|
action: action
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the data to the server
|
||||||
|
fetch('/record-selection', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to record selection');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Selection recorded:', data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error recording selection:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to load a new image from our local server
|
||||||
|
function loadNewImage() {
|
||||||
|
// Show loading state
|
||||||
|
const img = card.querySelector('img');
|
||||||
|
img.style.opacity = '0.5';
|
||||||
|
|
||||||
|
// Fetch a random image from our API
|
||||||
|
fetch('/random-image')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch image');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Store current image info
|
||||||
|
currentImageInfo = data;
|
||||||
|
|
||||||
|
// Extract filename from path
|
||||||
|
const pathParts = data.path.split('/');
|
||||||
|
const filename = pathParts[pathParts.length - 1];
|
||||||
|
currentImageInfo.filename = filename;
|
||||||
|
currentImageInfo.creation_date = data.creation_date || 'Unknown';
|
||||||
|
|
||||||
|
// Update the image source
|
||||||
|
img.onload = function() {
|
||||||
|
img.style.opacity = '1';
|
||||||
|
};
|
||||||
|
img.src = data.path;
|
||||||
|
|
||||||
|
// Update status with resolution info
|
||||||
|
const statusElement = document.querySelector('.status-area p:first-child');
|
||||||
|
statusElement.textContent = `Current resolution: ${data.resolution}`;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading image:', error);
|
||||||
|
img.style.opacity = '1';
|
||||||
|
img.src = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3EImage%20not%20found%3C%2Ftext%3E%3C%2Fsvg%3E';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle card click for viewing full-resolution image
|
||||||
|
function handleCardClick(e) {
|
||||||
|
// Only process click if we have image info and we're not in the middle of a swipe
|
||||||
|
if (!currentImageInfo || card.classList.contains('swiping')) return;
|
||||||
|
|
||||||
|
// Prevent click from propagating (important for touch devices)
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
|
||||||
|
// Set the full-resolution image source
|
||||||
|
fullscreenImage.src = currentImageInfo.path;
|
||||||
|
|
||||||
|
// Update modal info
|
||||||
|
modalResolution.textContent = `Resolution: ${currentImageInfo.resolution}`;
|
||||||
|
modalFilename.textContent = `Filename: ${currentImageInfo.filename || 'Unknown'}`;
|
||||||
|
modalCreationDate.textContent = `Creation Date: ${currentImageInfo.creation_date || 'Unknown'}`;
|
||||||
|
|
||||||
|
// Display the modal
|
||||||
|
modal.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial image
|
||||||
|
loadNewImage();
|
||||||
|
});
|
||||||
327
styles.css
Normal file
327
styles.css
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-link:hover {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-container {
|
||||||
|
position: relative;
|
||||||
|
height: 400px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
|
transform-origin: center center;
|
||||||
|
background-color: white;
|
||||||
|
touch-action: none; /* Prevents default touch actions like scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #999;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card.loading .loading-indicator {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card.swiping {
|
||||||
|
transition: none; /* Remove transition during active swiping */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card.swipe-left {
|
||||||
|
transform: translateX(-150%) rotate(-20deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card.swipe-right {
|
||||||
|
transform: translateX(150%) rotate(20deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card.swipe-up {
|
||||||
|
transform: translateY(-150%) rotate(5deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card.swipe-down {
|
||||||
|
transform: translateY(150%) rotate(-5deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-hint {
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-hint {
|
||||||
|
left: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-hint {
|
||||||
|
right: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #2ed573;
|
||||||
|
}
|
||||||
|
|
||||||
|
.up-hint {
|
||||||
|
top: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #1e90ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.down-hint {
|
||||||
|
bottom: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #ffa502;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-left {
|
||||||
|
background-color: #ff4757;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-right {
|
||||||
|
background-color: #2ed573;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-up {
|
||||||
|
background-color: #1e90ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-down {
|
||||||
|
background-color: #ffa502;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-area {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#last-action {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
display: inline-block;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-color {
|
||||||
|
background-color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-color {
|
||||||
|
background-color: #2ed573;
|
||||||
|
}
|
||||||
|
|
||||||
|
.up-color {
|
||||||
|
background-color: #1e90ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.down-color {
|
||||||
|
background-color: #ffa502;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal styles */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 90%;
|
||||||
|
height: 90%;
|
||||||
|
max-width: 1200px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fullscreen-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 85vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 20px;
|
||||||
|
color: white;
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal:hover,
|
||||||
|
.close-modal:focus {
|
||||||
|
color: #bbb;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-info {
|
||||||
|
margin-top: 15px;
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the card clickable */
|
||||||
|
.image-card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card::after {
|
||||||
|
content: '🔍';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
color: #333;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-card:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
247
venv/bin/Activate.ps1
Normal file
247
venv/bin/Activate.ps1
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Activate a Python virtual environment for the current PowerShell session.
|
||||||
|
|
||||||
|
.Description
|
||||||
|
Pushes the python executable for a virtual environment to the front of the
|
||||||
|
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||||
|
in a Python virtual environment. Makes use of the command line switches as
|
||||||
|
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||||
|
|
||||||
|
.Parameter VenvDir
|
||||||
|
Path to the directory that contains the virtual environment to activate. The
|
||||||
|
default value for this is the parent of the directory that the Activate.ps1
|
||||||
|
script is located within.
|
||||||
|
|
||||||
|
.Parameter Prompt
|
||||||
|
The prompt prefix to display when this virtual environment is activated. By
|
||||||
|
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||||
|
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Verbose
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and shows extra information about the activation as it executes.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||||
|
Activates the Python virtual environment located in the specified location.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Prompt "MyPython"
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and prefixes the current prompt with the specified string (surrounded in
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$VenvDir,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$Prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
<# Function declarations --------------------------------------------------- #>
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Remove all shell session elements added by the Activate script, including the
|
||||||
|
addition of the virtual environment's Python executable from the beginning of
|
||||||
|
the PATH variable.
|
||||||
|
|
||||||
|
.Parameter NonDestructive
|
||||||
|
If present, do not remove this function from the global namespace for the
|
||||||
|
session.
|
||||||
|
|
||||||
|
#>
|
||||||
|
function global:deactivate ([switch]$NonDestructive) {
|
||||||
|
# Revert to original values
|
||||||
|
|
||||||
|
# The prior prompt:
|
||||||
|
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||||
|
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||||
|
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PYTHONHOME:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PATH:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||||
|
given folder, and returns them in a map.
|
||||||
|
|
||||||
|
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||||
|
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||||
|
then it is considered a `key = value` line. The left hand string is the key,
|
||||||
|
the right hand is the value.
|
||||||
|
|
||||||
|
If the value starts with a `'` or a `"` then the first and last character is
|
||||||
|
stripped from the value before being captured.
|
||||||
|
|
||||||
|
.Parameter ConfigDir
|
||||||
|
Path to the directory that contains the `pyvenv.cfg` file.
|
||||||
|
#>
|
||||||
|
function Get-PyVenvConfig(
|
||||||
|
[String]
|
||||||
|
$ConfigDir
|
||||||
|
) {
|
||||||
|
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||||
|
|
||||||
|
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||||
|
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||||
|
|
||||||
|
# An empty map will be returned if no config file is found.
|
||||||
|
$pyvenvConfig = @{ }
|
||||||
|
|
||||||
|
if ($pyvenvConfigPath) {
|
||||||
|
|
||||||
|
Write-Verbose "File exists, parse `key = value` lines"
|
||||||
|
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||||
|
|
||||||
|
$pyvenvConfigContent | ForEach-Object {
|
||||||
|
$keyval = $PSItem -split "\s*=\s*", 2
|
||||||
|
if ($keyval[0] -and $keyval[1]) {
|
||||||
|
$val = $keyval[1]
|
||||||
|
|
||||||
|
# Remove extraneous quotations around a string value.
|
||||||
|
if ("'""".Contains($val.Substring(0, 1))) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pyvenvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<# Begin Activate script --------------------------------------------------- #>
|
||||||
|
|
||||||
|
# Determine the containing directory of this script
|
||||||
|
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||||
|
|
||||||
|
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||||
|
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||||
|
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||||
|
|
||||||
|
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||||
|
# First, get the location of the virtual environment, it might not be
|
||||||
|
# VenvExecDir if specified on the command line.
|
||||||
|
if ($VenvDir) {
|
||||||
|
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||||
|
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||||
|
|
||||||
|
# Next, set the prompt from the command line, or the config file, or
|
||||||
|
# just use the name of the virtual environment folder.
|
||||||
|
if ($Prompt) {
|
||||||
|
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||||
|
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||||
|
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||||
|
$Prompt = $pyvenvCfg['prompt'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||||
|
# that there is an activated venv.
|
||||||
|
$env:VIRTUAL_ENV = $VenvDir
|
||||||
|
|
||||||
|
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
|
||||||
|
Write-Verbose "Setting prompt to '$Prompt'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||||
|
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||||
|
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear PYTHONHOME
|
||||||
|
if (Test-Path -Path Env:PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
Remove-Item -Path Env:PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the venv to the PATH
|
||||||
|
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
69
venv/bin/activate
Normal file
69
venv/bin/activate
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# you cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This should detect bash and zsh, which have a hash command that must
|
||||||
|
# be called to get it to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
VIRTUAL_ENV="/home/aodhan/Projects/python/venv"
|
||||||
|
export VIRTUAL_ENV
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1="(venv) ${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT="(venv) "
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This should detect bash and zsh, which have a hash command that must
|
||||||
|
# be called to get it to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
fi
|
||||||
26
venv/bin/activate.csh
Normal file
26
venv/bin/activate.csh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV "/home/aodhan/Projects/python/venv"
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
set prompt = "(venv) $prompt"
|
||||||
|
setenv VIRTUAL_ENV_PROMPT "(venv) "
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
66
venv/bin/activate.fish
Normal file
66
venv/bin/activate.fish
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||||
|
# (https://fishshell.com/); you cannot run it directly.
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
functions -e fish_prompt
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV "/home/aodhan/Projects/python/venv"
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
||||||
|
|
||||||
|
# Unset PYTHONHOME if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# With the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command.
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
|
printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
# Output the original/"old" prompt.
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT "(venv) "
|
||||||
|
end
|
||||||
8
venv/bin/flask
Executable file
8
venv/bin/flask
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/aodhan/Projects/python/venv/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from flask.cli import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
venv/bin/pip
Executable file
8
venv/bin/pip
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/aodhan/Projects/python/venv/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
venv/bin/pip3
Executable file
8
venv/bin/pip3
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/aodhan/Projects/python/venv/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
venv/bin/pip3.10
Executable file
8
venv/bin/pip3.10
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/aodhan/Projects/python/venv/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
1
venv/bin/python
Symbolic link
1
venv/bin/python
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/home/aodhan/.pyenv/versions/3.10.6/bin/python
|
||||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
python
|
||||||
1
venv/bin/python3.10
Symbolic link
1
venv/bin/python3.10
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
python
|
||||||
1
venv/lib64
Symbolic link
1
venv/lib64
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
lib
|
||||||
3
venv/pyvenv.cfg
Normal file
3
venv/pyvenv.cfg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
home = /home/aodhan/.pyenv/versions/3.10.6/bin
|
||||||
|
include-system-site-packages = false
|
||||||
|
version = 3.10.6
|
||||||
Reference in New Issue
Block a user