Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>QuickSnap - Fast Photo Capture</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .camera-container { | |
| position: relative; | |
| overflow: hidden; | |
| border-radius: 1rem; | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); | |
| } | |
| .camera-view { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| transform: scaleX(-1); | |
| } | |
| .flash { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: white; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s; | |
| } | |
| .flash.active { | |
| opacity: 0.8; | |
| } | |
| .category-chip { | |
| transition: all 0.2s; | |
| } | |
| .category-chip:hover { | |
| transform: scale(1.05); | |
| } | |
| .category-chip.active { | |
| background-color: #3b82f6; | |
| color: white; | |
| } | |
| .snap-btn { | |
| transition: all 0.2s; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .snap-btn:active { | |
| transform: scale(0.95); | |
| } | |
| .photo-thumbnail { | |
| transition: all 0.2s; | |
| } | |
| .photo-thumbnail:hover { | |
| transform: scale(1.03); | |
| } | |
| .modal { | |
| transition: opacity 0.3s, visibility 0.3s; | |
| } | |
| .drawer { | |
| transition: transform 0.3s; | |
| } | |
| .drawer.closed { | |
| transform: translateX(100%); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="flex justify-between items-center mb-8"> | |
| <h1 class="text-3xl font-bold text-blue-600">QuickSnap</h1> | |
| <button id="settings-btn" class="p-2 rounded-full hover:bg-gray-200"> | |
| <i class="fas fa-cog text-gray-600 text-xl"></i> | |
| </button> | |
| </header> | |
| <!-- Main Content --> | |
| <main> | |
| <!-- Camera Section --> | |
| <div class="mb-8"> | |
| <div class="camera-container bg-gray-200 aspect-video relative mb-4"> | |
| <video id="camera-view" class="camera-view" autoplay playsinline></video> | |
| <div id="flash" class="flash"></div> | |
| </div> | |
| <div class="flex justify-center"> | |
| <button id="snap-btn" class="snap-btn bg-blue-600 text-white rounded-full p-4 hover:bg-blue-700 focus:outline-none"> | |
| <i class="fas fa-camera text-2xl"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Categories Section --> | |
| <div class="mb-8"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-800">Categories</h2> | |
| <button id="add-category-btn" class="text-blue-600 hover:text-blue-800"> | |
| <i class="fas fa-plus mr-1"></i> Add | |
| </button> | |
| </div> | |
| <div id="categories-container" class="flex flex-wrap gap-2"> | |
| <!-- Categories will be added here dynamically --> | |
| </div> | |
| </div> | |
| <!-- Gallery Section --> | |
| <div> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Your Snaps</h2> | |
| <div id="gallery-container" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4"> | |
| <!-- Photos will be added here dynamically --> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Add Category Modal --> | |
| <div id="add-category-modal" class="modal fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 invisible z-50"> | |
| <div class="bg-white rounded-lg p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-semibold">Add New Category</h3> | |
| <button id="close-category-modal" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <input id="category-name-input" type="text" placeholder="Category name" class="w-full px-4 py-2 border rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <div class="flex justify-end space-x-2"> | |
| <button id="cancel-category-btn" class="px-4 py-2 border rounded-lg hover:bg-gray-100">Cancel</button> | |
| <button id="save-category-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Drawer --> | |
| <div id="settings-drawer" class="drawer fixed top-0 right-0 h-full w-64 bg-white shadow-lg z-50 p-4 transform translate-x-full"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h3 class="text-xl font-semibold">Settings</h3> | |
| <button id="close-settings-btn" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-gray-700 mb-2">Flash</label> | |
| <select id="flash-setting" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="auto">Auto</option> | |
| <option value="on">On</option> | |
| <option value="off">Off</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-2">Camera</label> | |
| <select id="camera-setting" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <!-- Camera options will be added dynamically --> | |
| </select> | |
| </div> | |
| <div> | |
| <button id="clear-storage-btn" class="w-full px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"> | |
| Clear All Data | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const cameraView = document.getElementById('camera-view'); | |
| const snapBtn = document.getElementById('snap-btn'); | |
| const flash = document.getElementById('flash'); | |
| const categoriesContainer = document.getElementById('categories-container'); | |
| const galleryContainer = document.getElementById('gallery-container'); | |
| const addCategoryBtn = document.getElementById('add-category-btn'); | |
| const addCategoryModal = document.getElementById('add-category-modal'); | |
| const closeCategoryModal = document.getElementById('close-category-modal'); | |
| const cancelCategoryBtn = document.getElementById('cancel-category-btn'); | |
| const saveCategoryBtn = document.getElementById('save-category-btn'); | |
| const categoryNameInput = document.getElementById('category-name-input'); | |
| const settingsBtn = document.getElementById('settings-btn'); | |
| const settingsDrawer = document.getElementById('settings-drawer'); | |
| const closeSettingsBtn = document.getElementById('close-settings-btn'); | |
| const flashSetting = document.getElementById('flash-setting'); | |
| const cameraSetting = document.getElementById('camera-setting'); | |
| const clearStorageBtn = document.getElementById('clear-storage-btn'); | |
| // App State | |
| let currentCategory = 'default'; | |
| let mediaStream = null; | |
| let cameras = []; | |
| let selectedCameraId = null; | |
| // Initialize the app | |
| initApp(); | |
| // Functions | |
| async function initApp() { | |
| // Load categories and photos from localStorage | |
| loadCategories(); | |
| loadPhotos(); | |
| // Initialize camera | |
| await initCamera(); | |
| // Set up event listeners | |
| setupEventListeners(); | |
| } | |
| async function initCamera() { | |
| try { | |
| // Stop any existing stream | |
| if (mediaStream) { | |
| mediaStream.getTracks().forEach(track => track.stop()); | |
| } | |
| // Get available cameras | |
| const devices = await navigator.mediaDevices.enumerateDevices(); | |
| cameras = devices.filter(device => device.kind === 'videoinput'); | |
| // Update camera selector | |
| updateCameraSelector(); | |
| // Start with default camera (or the one saved in settings) | |
| const savedCameraId = localStorage.getItem('selectedCameraId'); | |
| const cameraId = savedCameraId || (cameras.length > 0 ? cameras[0].deviceId : null); | |
| if (cameraId) { | |
| await startCamera(cameraId); | |
| selectedCameraId = cameraId; | |
| cameraSetting.value = cameraId; | |
| } | |
| } catch (error) { | |
| console.error('Error initializing camera:', error); | |
| alert('Could not access the camera. Please make sure you have granted camera permissions.'); | |
| } | |
| } | |
| async function startCamera(deviceId) { | |
| const constraints = { | |
| video: { | |
| deviceId: { exact: deviceId }, | |
| width: { ideal: 1280 }, | |
| height: { ideal: 720 }, | |
| facingMode: 'environment' | |
| } | |
| }; | |
| try { | |
| mediaStream = await navigator.mediaDevices.getUserMedia(constraints); | |
| cameraView.srcObject = mediaStream; | |
| } catch (error) { | |
| console.error('Error starting camera:', error); | |
| // Fallback to any camera if the selected one fails | |
| if (deviceId !== cameras[0]?.deviceId) { | |
| await startCamera(cameras[0]?.deviceId); | |
| } | |
| } | |
| } | |
| function updateCameraSelector() { | |
| cameraSetting.innerHTML = ''; | |
| cameras.forEach(camera => { | |
| const option = document.createElement('option'); | |
| option.value = camera.deviceId; | |
| option.text = camera.label || `Camera ${cameraSetting.options.length + 1}`; | |
| cameraSetting.appendChild(option); | |
| }); | |
| } | |
| function loadCategories() { | |
| // Load categories from localStorage | |
| const categories = JSON.parse(localStorage.getItem('categories')) || ['default', 'work', 'personal']; | |
| // Clear existing categories | |
| categoriesContainer.innerHTML = ''; | |
| // Add each category | |
| categories.forEach(category => { | |
| addCategoryToUI(category); | |
| }); | |
| // Set the first category as active | |
| if (categories.length > 0) { | |
| setActiveCategory(categories[0]); | |
| } | |
| } | |
| function addCategoryToUI(category) { | |
| const categoryElement = document.createElement('div'); | |
| categoryElement.className = `category-chip px-4 py-2 bg-gray-200 rounded-full cursor-pointer ${currentCategory === category ? 'active' : ''}`; | |
| categoryElement.textContent = category; | |
| categoryElement.dataset.category = category; | |
| categoryElement.addEventListener('click', () => { | |
| setActiveCategory(category); | |
| }); | |
| categoriesContainer.appendChild(categoryElement); | |
| } | |
| function setActiveCategory(category) { | |
| // Update active state in UI | |
| document.querySelectorAll('.category-chip').forEach(chip => { | |
| chip.classList.toggle('active', chip.dataset.category === category); | |
| }); | |
| // Update current category | |
| currentCategory = category; | |
| // Reload photos for this category | |
| loadPhotos(); | |
| } | |
| function loadPhotos() { | |
| // Load photos from localStorage for the current category | |
| const allPhotos = JSON.parse(localStorage.getItem('photos')) || {}; | |
| const categoryPhotos = allPhotos[currentCategory] || []; | |
| // Clear gallery | |
| galleryContainer.innerHTML = ''; | |
| // Add each photo to the gallery | |
| categoryPhotos.forEach((photoData, index) => { | |
| addPhotoToGallery(photoData, index); | |
| }); | |
| } | |
| function addPhotoToGallery(photoData, index) { | |
| const photoElement = document.createElement('div'); | |
| photoElement.className = 'photo-thumbnail bg-white rounded-lg overflow-hidden shadow-md hover:shadow-lg'; | |
| const img = document.createElement('img'); | |
| img.src = photoData.image; | |
| img.alt = `Snap ${index + 1}`; | |
| img.className = 'w-full h-32 object-cover'; | |
| const footer = document.createElement('div'); | |
| footer.className = 'p-2 text-sm text-gray-700 truncate'; | |
| footer.textContent = new Date(photoData.timestamp).toLocaleString(); | |
| photoElement.appendChild(img); | |
| photoElement.appendChild(footer); | |
| galleryContainer.appendChild(photoElement); | |
| } | |
| function takeSnapshot() { | |
| // Create canvas to capture the photo | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = cameraView.videoWidth; | |
| canvas.height = cameraView.videoHeight; | |
| const ctx = canvas.getContext('2d'); | |
| // Draw the current frame to canvas | |
| ctx.drawImage(cameraView, 0, 0, canvas.width, canvas.height); | |
| // Get the image data | |
| const imageData = canvas.toDataURL('image/png'); | |
| // Create photo object | |
| const photo = { | |
| image: imageData, | |
| category: currentCategory, | |
| timestamp: Date.now() | |
| }; | |
| // Save to localStorage | |
| savePhoto(photo); | |
| // Add to gallery | |
| addPhotoToGallery(photo, galleryContainer.children.length); | |
| // Flash effect | |
| if (flashSetting.value !== 'off') { | |
| flash.classList.add('active'); | |
| setTimeout(() => { | |
| flash.classList.remove('active'); | |
| }, 200); | |
| } | |
| } | |
| function savePhoto(photo) { | |
| // Load existing photos | |
| const allPhotos = JSON.parse(localStorage.getItem('photos')) || {}; | |
| // Initialize category if it doesn't exist | |
| if (!allPhotos[photo.category]) { | |
| allPhotos[photo.category] = []; | |
| } | |
| // Add the new photo | |
| allPhotos[photo.category].unshift(photo); // Add to beginning of array | |
| // Save back to localStorage | |
| localStorage.setItem('photos', JSON.stringify(allPhotos)); | |
| } | |
| function addNewCategory(name) { | |
| // Validate name | |
| if (!name.trim()) { | |
| alert('Please enter a category name'); | |
| return; | |
| } | |
| // Load existing categories | |
| const categories = JSON.parse(localStorage.getItem('categories')) || ['default', 'work', 'personal']; | |
| // Check if category already exists | |
| if (categories.includes(name)) { | |
| alert('This category already exists'); | |
| return; | |
| } | |
| // Add new category | |
| categories.push(name); | |
| // Save to localStorage | |
| localStorage.setItem('categories', JSON.stringify(categories)); | |
| // Add to UI | |
| addCategoryToUI(name); | |
| // Close modal and clear input | |
| toggleModal(addCategoryModal, false); | |
| categoryNameInput.value = ''; | |
| } | |
| function clearAllData() { | |
| if (confirm('Are you sure you want to clear all photos and categories? This cannot be undone.')) { | |
| localStorage.removeItem('photos'); | |
| localStorage.removeItem('categories'); | |
| localStorage.removeItem('selectedCameraId'); | |
| loadCategories(); | |
| loadPhotos(); | |
| toggleModal(settingsDrawer, false); | |
| } | |
| } | |
| function toggleModal(modal, show) { | |
| if (show) { | |
| modal.classList.remove('invisible', 'opacity-0'); | |
| if (modal === settingsDrawer) { | |
| modal.classList.remove('closed'); | |
| } | |
| } else { | |
| modal.classList.add('invisible', 'opacity-0'); | |
| if (modal === settingsDrawer) { | |
| modal.classList.add('closed'); | |
| } | |
| } | |
| } | |
| function setupEventListeners() { | |
| // Snap button | |
| snapBtn.addEventListener('click', takeSnapshot); | |
| // Add category | |
| addCategoryBtn.addEventListener('click', () => toggleModal(addCategoryModal, true)); | |
| closeCategoryModal.addEventListener('click', () => toggleModal(addCategoryModal, false)); | |
| cancelCategoryBtn.addEventListener('click', () => toggleModal(addCategoryModal, false)); | |
| saveCategoryBtn.addEventListener('click', () => addNewCategory(categoryNameInput.value)); | |
| // Settings | |
| settingsBtn.addEventListener('click', () => toggleModal(settingsDrawer, true)); | |
| closeSettingsBtn.addEventListener('click', () => toggleModal(settingsDrawer, false)); | |
| // Flash setting | |
| flashSetting.addEventListener('change', () => { | |
| // Save preference (though we don't need to save to localStorage for this demo) | |
| }); | |
| // Camera setting | |
| cameraSetting.addEventListener('change', async () => { | |
| selectedCameraId = cameraSetting.value; | |
| localStorage.setItem('selectedCameraId', selectedCameraId); | |
| await startCamera(selectedCameraId); | |
| }); | |
| // Clear storage | |
| clearStorageBtn.addEventListener('click', clearAllData); | |
| // Keyboard shortcut for snapping (Spacebar) | |
| document.addEventListener('keydown', (e) => { | |
| if (e.code === 'Space') { | |
| e.preventDefault(); | |
| takeSnapshot(); | |
| } | |
| }); | |
| } | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Ultronprime/snapitraph" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |