snapitraph / index.html
Ultronprime's picture
Add 3 files
b974798 verified
<!DOCTYPE html>
<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>