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