diff --git a/app/templates/capture.html b/app/templates/capture.html index 2a896b4..b5d17c5 100644 --- a/app/templates/capture.html +++ b/app/templates/capture.html @@ -14,6 +14,7 @@ 📷 Take Photo + - - -
@@ -34,9 +30,34 @@

No photos captured yet

+ - -
+ + {% endblock %} @@ -77,33 +98,6 @@ cursor: not-allowed; } -.capture-session-bar { - margin-top: 0.75rem; - padding: 0.5rem 1rem; - background: var(--bg-secondary); - border-radius: 8px; - color: var(--accent); - font-weight: 500; - font-size: 0.95rem; - display: inline-block; -} - -.capture-toast { - position: fixed; - top: 70px; - left: 50%; - transform: translateX(-50%); - padding: 0.6rem 1.25rem; - border-radius: 8px; - font-size: 0.9rem; - z-index: 9998; - display: none; - white-space: nowrap; - background: rgba(40, 167, 69, 0.9); - color: #fff; - pointer-events: none; -} - .recent-photos { margin-top: 3rem; padding-bottom: 2rem; @@ -164,6 +158,142 @@ .thumbnail .status-badge.verified { background: var(--success); } + +/* Camera Viewfinder */ +body.camera-open { + overflow: hidden; +} + +.camera-viewfinder { + position: fixed; + inset: 0; + background: #000; + z-index: 9999; + display: flex; + flex-direction: column; +} + +.camera-viewfinder video { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.camera-flash { + position: absolute; + inset: 0; + background: #fff; + opacity: 0; + pointer-events: none; + z-index: 2; + transition: opacity 0.05s; +} + +.camera-flash.flash { + opacity: 0.6; +} + +.camera-top-bar { + position: relative; + z-index: 3; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1rem; + padding-top: max(0.5rem, env(safe-area-inset-top)); + background: rgba(0,0,0,0.4); +} + +.camera-ui-btn { + background: none; + border: none; + color: #fff; + font-size: 1.5rem; + padding: 0.5rem; + min-width: 48px; + min-height: 48px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.camera-close-btn { + font-size: 2.2rem; + line-height: 1; +} + +.camera-counter { + color: #fff; + font-size: 1rem; + font-weight: 600; +} + +.camera-bottom-bar { + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 3; + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem 2rem; + padding-bottom: max(1.5rem, env(safe-area-inset-bottom)); + background: rgba(0,0,0,0.4); +} + +.camera-shutter-btn { + width: 72px; + height: 72px; + border-radius: 50%; + border: 4px solid #fff; + background: rgba(255,255,255,0.2); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + +.camera-shutter-btn:active { + transform: scale(0.92); +} + +.camera-shutter-btn:disabled { + opacity: 0.4; +} + +.shutter-ring { + display: block; + width: 56px; + height: 56px; + border-radius: 50%; + background: #fff; +} + +.camera-flip-btn { + font-size: 1.5rem; +} + +.camera-toast { + position: absolute; + bottom: 140px; + left: 50%; + transform: translateX(-50%); + padding: 0.5rem 1.25rem; + border-radius: 8px; + font-size: 0.9rem; + z-index: 4; + display: none; + white-space: nowrap; + background: rgba(40, 167, 69, 0.85); + color: #fff; + pointer-events: none; +} {% endblock %} @@ -191,111 +321,194 @@ Storage.init().then(() => { const captureBtn = document.getElementById('capture-btn'); const cameraInput = document.getElementById('camera-input'); -// Continuous capture state +// ---- Camera state ---- +let cameraStream = null; +let facingMode = 'environment'; let sessionCount = 0; -let continuousCapture = false; -let processingPhoto = false; +let shutterBusy = false; -captureBtn.addEventListener('click', function() { - sessionCount = 0; - updateSessionCounter(); - cameraInput.click(); +// ---- Main button: open live viewfinder, fallback to file input ---- +captureBtn.addEventListener('click', async function() { + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + await openCamera(); + } else { + // Fallback: use file input + cameraInput.click(); + } }); +// ---- File input fallback (single-shot) ---- cameraInput.addEventListener('change', async function(e) { const file = e.target.files[0]; - if (!file) { - // User cancelled the camera — stop continuous capture - continuousCapture = false; - if (sessionCount > 0) { - showCaptureToast(sessionCount + ' photo' + (sessionCount !== 1 ? 's' : '') + ' captured'); - } - return; - } + if (!file) return; - processingPhoto = true; captureBtn.disabled = true; captureBtn.textContent = '⏳ Processing...'; try { const jpegBlob = await convertToJPEG(file); - - const now = new Date(); - const timestamp = now.getFullYear() + - String(now.getMonth() + 1).padStart(2, '0') + - String(now.getDate()).padStart(2, '0') + '_' + - String(now.getHours()).padStart(2, '0') + - String(now.getMinutes()).padStart(2, '0') + - String(now.getSeconds()).padStart(2, '0'); - const filename = currentUsername + '_' + timestamp + '.jpg'; - const targetPath = localStorage.getItem('nextsnap_upload_path') || '/'; - - if (storageReady) { - await Storage.savePhoto({ - username: currentUsername, - timestamp: Date.now(), - filename: filename, - targetPath: targetPath, - blob: jpegBlob, - status: 'pending' - }); - - if (navigator.onLine && typeof SyncEngine !== 'undefined') { - SyncEngine.triggerSync(); - } - - loadRecentPhotos(); - } - - sessionCount++; - continuousCapture = true; - updateSessionCounter(); - showCaptureToast('Photo saved'); - - captureBtn.disabled = false; - captureBtn.textContent = '📷 Take Photo'; - - } catch (error) { - console.error('Error:', error); - captureBtn.textContent = '❌ Error'; - continuousCapture = false; + await savePhoto(jpegBlob); + captureBtn.textContent = '✓ Photo Saved!'; + loadRecentPhotos(); setTimeout(() => { captureBtn.disabled = false; captureBtn.textContent = '📷 Take Photo'; - }, 2000); + }, 1000); + } catch (error) { + console.error('Error:', error); alert('Failed: ' + error.message); + captureBtn.disabled = false; + captureBtn.textContent = '📷 Take Photo'; } - processingPhoto = false; e.target.value = ''; - - // Re-open camera automatically for next photo - if (continuousCapture) { - setTimeout(() => { - cameraInput.click(); - }, 300); - } }); -function updateSessionCounter() { - const bar = document.getElementById('capture-session-bar'); - const countEl = document.getElementById('session-count'); - if (sessionCount > 0) { - countEl.textContent = sessionCount; - bar.style.display = 'inline-block'; - } else { - bar.style.display = 'none'; +// ---- Live camera viewfinder ---- +async function openCamera() { + sessionCount = 0; + updateCameraCounter(); + + try { + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: facingMode, width: { ideal: 3840 }, height: { ideal: 2160 } }, + audio: false + }); + + const video = document.getElementById('camera-video'); + video.srcObject = cameraStream; + await video.play(); + + document.getElementById('camera-viewfinder').style.display = 'flex'; + document.body.classList.add('camera-open'); + } catch (err) { + console.error('getUserMedia failed:', err); + // Fallback to file input + cameraInput.click(); } } -function showCaptureToast(msg) { - const toast = document.getElementById('capture-toast'); - toast.textContent = '✓ ' + msg; +function closeCamera() { + if (cameraStream) { + cameraStream.getTracks().forEach(t => t.stop()); + cameraStream = null; + } + + const video = document.getElementById('camera-video'); + video.srcObject = null; + + document.getElementById('camera-viewfinder').style.display = 'none'; + document.body.classList.remove('camera-open'); + + loadRecentPhotos(); +} + +async function flipCamera() { + facingMode = facingMode === 'environment' ? 'user' : 'environment'; + + if (cameraStream) { + cameraStream.getTracks().forEach(t => t.stop()); + } + + try { + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: facingMode, width: { ideal: 3840 }, height: { ideal: 2160 } }, + audio: false + }); + const video = document.getElementById('camera-video'); + video.srcObject = cameraStream; + await video.play(); + } catch (err) { + console.error('Flip camera failed:', err); + showCameraToast('Could not switch camera'); + } +} + +async function takePhoto() { + if (shutterBusy) return; + shutterBusy = true; + + const shutterBtn = document.getElementById('camera-shutter-btn'); + shutterBtn.disabled = true; + + try { + const video = document.getElementById('camera-video'); + const canvas = document.getElementById('camera-canvas'); + + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(video, 0, 0); + + // Flash feedback + const flash = document.getElementById('camera-flash'); + flash.classList.add('flash'); + setTimeout(() => flash.classList.remove('flash'), 150); + + const blob = await new Promise((resolve, reject) => { + canvas.toBlob(b => b ? resolve(b) : reject(new Error('Canvas capture failed')), 'image/jpeg', 0.92); + }); + + await savePhoto(blob); + sessionCount++; + updateCameraCounter(); + showCameraToast('Photo saved'); + + } catch (err) { + console.error('takePhoto error:', err); + showCameraToast('Error saving photo'); + } finally { + shutterBusy = false; + shutterBtn.disabled = false; + } +} + +// ---- Wire viewfinder buttons ---- +document.getElementById('camera-close-btn').addEventListener('click', closeCamera); +document.getElementById('camera-shutter-btn').addEventListener('click', takePhoto); +document.getElementById('camera-flip-btn').addEventListener('click', flipCamera); + +function updateCameraCounter() { + const el = document.getElementById('camera-counter'); + el.textContent = sessionCount > 0 ? sessionCount + ' photo' + (sessionCount !== 1 ? 's' : '') : ''; +} + +function showCameraToast(msg) { + const toast = document.getElementById('camera-toast'); + toast.textContent = msg; toast.style.display = 'block'; setTimeout(() => { toast.style.display = 'none'; }, 1500); } -const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10MB +// ---- Shared helpers ---- +async function savePhoto(jpegBlob) { + const now = new Date(); + const timestamp = now.getFullYear() + + String(now.getMonth() + 1).padStart(2, '0') + + String(now.getDate()).padStart(2, '0') + '_' + + String(now.getHours()).padStart(2, '0') + + String(now.getMinutes()).padStart(2, '0') + + String(now.getSeconds()).padStart(2, '0'); + const filename = currentUsername + '_' + timestamp + '.jpg'; + const targetPath = localStorage.getItem('nextsnap_upload_path') || '/'; + + if (storageReady) { + await Storage.savePhoto({ + username: currentUsername, + timestamp: Date.now(), + filename: filename, + targetPath: targetPath, + blob: jpegBlob, + status: 'pending' + }); + + if (navigator.onLine && typeof SyncEngine !== 'undefined') { + SyncEngine.triggerSync(); + } + } +} + +const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; function convertToJPEG(file) { return new Promise((resolve, reject) => { @@ -318,10 +531,8 @@ function convertToJPEG(file) { return; } if (blob.size <= MAX_UPLOAD_BYTES || quality <= 0.3) { - console.log('Photo: ' + (blob.size / 1024 / 1024).toFixed(1) + 'MB, ' + canvas.width + 'x' + canvas.height + ', q=' + quality.toFixed(2)); resolve(blob); } else { - console.log('Photo too large (' + (blob.size / 1024 / 1024).toFixed(1) + 'MB), reducing...'); if (quality > 0.5) { quality -= 0.1; } else { @@ -368,40 +579,5 @@ async function loadRecentPhotos() { } console.log('Capture page loaded'); - -// Debug panel -setTimeout(() => { - const debugDiv = document.createElement('div'); - debugDiv.id = 'debug-panel'; - debugDiv.style.cssText = 'position:fixed;bottom:80px;right:10px;z-index:9999;background:black;color:lime;padding:10px;border-radius:5px;font-family:monospace;font-size:10px;max-width:300px;'; - - const status = []; - status.push('Storage: ' + (typeof Storage !== 'undefined' ? 'OK' : 'FAIL')); - status.push('SyncEngine: ' + (typeof SyncEngine !== 'undefined' ? 'OK' : 'FAIL')); - status.push('Dexie: ' + (typeof Dexie !== 'undefined' ? 'OK' : 'FAIL')); - - debugDiv.innerHTML = '
' + status.join('
') + '
' + - '' + - '
'; - document.body.appendChild(debugDiv); -}, 2000); - -window.manualSync = function() { - const statusDiv = document.getElementById('sync-status'); - statusDiv.textContent = 'Checking...'; - - if (typeof SyncEngine === 'undefined') { - statusDiv.textContent = 'ERROR: SyncEngine not loaded!'; - return; - } - - statusDiv.textContent = 'Triggering sync...'; - try { - SyncEngine.triggerSync(); - statusDiv.textContent = 'Sync triggered!'; - } catch (e) { - statusDiv.textContent = 'ERROR: ' + e.message; - } -}; {% endblock %}