commit ad0e64e000509620022115f0a785ef190829726b Author: Aodhan Date: Wed May 28 23:11:50 2025 +0100 Initial commit: Image Swipe App with SQLite database diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..719823f --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..df137d8 --- /dev/null +++ b/README.md @@ -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 diff --git a/app.py b/app.py new file mode 100644 index 0000000..3e12535 --- /dev/null +++ b/app.py @@ -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() diff --git a/history.html b/history.html new file mode 100644 index 0000000..dd8ebf9 --- /dev/null +++ b/history.html @@ -0,0 +1,628 @@ + + + + + + Image Selection History + + + + + + + +
+
+

Image Selection History

+
+ + Back to Swiper +
+
+ + + + +
+ + + + + +
+ +
+ +
Loading selections...
+
+
+ + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..ef4746a --- /dev/null +++ b/index.html @@ -0,0 +1,66 @@ + + + + + + Image Swipe App + + + +
+
+

Image Swiper

+

Swipe to sort your images

+ View History +
+ +
+
+ + Image +
Loading...
+
+ +
+
← Swipe Left
+
Swipe Right →
+
↑ Swipe Up
+
Swipe Down ↓
+
+
+ +
+ + + + +
+ +
+

Current resolution: Loading...

+

Last action: None

+
+
Left: Discard
+
Right: Keep
+
Up: Favorite
+
Down: Review Later
+
+
+
+ + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..df96723 --- /dev/null +++ b/script.js @@ -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(); +}); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..480a0da --- /dev/null +++ b/styles.css @@ -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; +} diff --git a/venv/bin/Activate.ps1 b/venv/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/venv/bin/Activate.ps1 @@ -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" diff --git a/venv/bin/activate b/venv/bin/activate new file mode 100644 index 0000000..f97fcca --- /dev/null +++ b/venv/bin/activate @@ -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 diff --git a/venv/bin/activate.csh b/venv/bin/activate.csh new file mode 100644 index 0000000..e66776b --- /dev/null +++ b/venv/bin/activate.csh @@ -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 . +# Ported to Python 3.3 venv by Andrew Svetlov + +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 diff --git a/venv/bin/activate.fish b/venv/bin/activate.fish new file mode 100644 index 0000000..030d17a --- /dev/null +++ b/venv/bin/activate.fish @@ -0,0 +1,66 @@ +# This file must be used with "source /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 diff --git a/venv/bin/flask b/venv/bin/flask new file mode 100755 index 0000000..a282111 --- /dev/null +++ b/venv/bin/flask @@ -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()) diff --git a/venv/bin/pip b/venv/bin/pip new file mode 100755 index 0000000..3217d79 --- /dev/null +++ b/venv/bin/pip @@ -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()) diff --git a/venv/bin/pip3 b/venv/bin/pip3 new file mode 100755 index 0000000..3217d79 --- /dev/null +++ b/venv/bin/pip3 @@ -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()) diff --git a/venv/bin/pip3.10 b/venv/bin/pip3.10 new file mode 100755 index 0000000..3217d79 --- /dev/null +++ b/venv/bin/pip3.10 @@ -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()) diff --git a/venv/bin/python b/venv/bin/python new file mode 120000 index 0000000..4321f93 --- /dev/null +++ b/venv/bin/python @@ -0,0 +1 @@ +/home/aodhan/.pyenv/versions/3.10.6/bin/python \ No newline at end of file diff --git a/venv/bin/python3 b/venv/bin/python3 new file mode 120000 index 0000000..d8654aa --- /dev/null +++ b/venv/bin/python3 @@ -0,0 +1 @@ +python \ No newline at end of file diff --git a/venv/bin/python3.10 b/venv/bin/python3.10 new file mode 120000 index 0000000..d8654aa --- /dev/null +++ b/venv/bin/python3.10 @@ -0,0 +1 @@ +python \ No newline at end of file diff --git a/venv/lib64 b/venv/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/venv/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/venv/pyvenv.cfg b/venv/pyvenv.cfg new file mode 100644 index 0000000..e4bdba0 --- /dev/null +++ b/venv/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /home/aodhan/.pyenv/versions/3.10.6/bin +include-system-site-packages = false +version = 3.10.6