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
+
-
-
-
- 0 photos this session
-
@@ -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 %}