// NextSnap - Camera Capture and JPEG Conversion 'use strict'; const Camera = { input: null, currentUsername: null, init(username) { this.currentUsername = username; this.input = document.getElementById('camera-input'); const captureBtn = document.getElementById('capture-btn'); if (this.input && captureBtn) { captureBtn.addEventListener('click', () => this.triggerCapture()); this.input.addEventListener('change', (e) => this.handleCapture(e)); } }, triggerCapture() { if (this.input) { this.input.click(); } }, async handleCapture(event) { const file = event.target.files[0]; if (!file) return; try { // Show loading state this.showCaptureLoading(); // Convert to JPEG const jpegBlob = await this.convertToJPEG(file); // Generate filename const filename = this.generateFilename(); // Get target path const targetPath = localStorage.getItem('nextsnap_upload_path') || '/'; // Save to IndexedDB const photoId = await Storage.savePhoto({ username: this.currentUsername, timestamp: Date.now(), filename: filename, targetPath: targetPath, blob: jpegBlob, status: 'pending' }); // Show success feedback this.showCaptureSuccess(jpegBlob); // Update recent photos display this.updateRecentPhotos(); // Update pending count this.updatePendingCount(); // Clear input for next capture event.target.value = ''; // Trigger sync if online (will be implemented in Phase 7) if (navigator.onLine && typeof Sync !== 'undefined') { Sync.triggerSync(); } } catch (error) { console.error('Error capturing photo:', error); this.showCaptureError(error.message); } }, async convertToJPEG(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async (e) => { try { const img = new Image(); img.onload = async () => { try { // Get EXIF orientation const orientation = await this.getOrientation(file); // Create canvas const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // Set canvas dimensions based on orientation let width = img.width; let height = img.height; if (orientation >= 5 && orientation <= 8) { // Swap dimensions for rotated images canvas.width = height; canvas.height = width; } else { canvas.width = width; canvas.height = height; } // Apply orientation transformation this.applyOrientation(ctx, orientation, width, height); // Draw image ctx.drawImage(img, 0, 0, width, height); // Convert to JPEG blob canvas.toBlob( (blob) => { if (blob) { resolve(blob); } else { reject(new Error('Failed to convert image to JPEG')); } }, 'image/jpeg', 0.92 ); } catch (error) { reject(error); } }; img.onerror = () => { reject(new Error('Failed to load image')); }; img.src = e.target.result; } catch (error) { reject(error); } }; reader.onerror = () => { reject(new Error('Failed to read file')); }; reader.readAsDataURL(file); }); }, async getOrientation(file) { // Simple EXIF orientation detection // For production, consider using a library like exif-js return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { const view = new DataView(e.target.result); if (view.getUint16(0, false) !== 0xFFD8) { resolve(1); // Not a JPEG return; } const length = view.byteLength; let offset = 2; while (offset < length) { if (view.getUint16(offset + 2, false) <= 8) { resolve(1); return; } const marker = view.getUint16(offset, false); offset += 2; if (marker === 0xFFE1) { // EXIF marker const little = view.getUint16(offset + 8, false) === 0x4949; offset += 10; const tags = view.getUint16(offset, little); offset += 2; for (let i = 0; i < tags; i++) { if (view.getUint16(offset + (i * 12), little) === 0x0112) { resolve(view.getUint16(offset + (i * 12) + 8, little)); return; } } } else if ((marker & 0xFF00) !== 0xFF00) { break; } else { offset += view.getUint16(offset, false); } } resolve(1); // Default orientation }; reader.onerror = () => resolve(1); reader.readAsArrayBuffer(file.slice(0, 64 * 1024)); }); }, applyOrientation(ctx, orientation, width, height) { switch (orientation) { case 2: // Horizontal flip ctx.transform(-1, 0, 0, 1, width, 0); break; case 3: // 180° rotate ctx.transform(-1, 0, 0, -1, width, height); break; case 4: // Vertical flip ctx.transform(1, 0, 0, -1, 0, height); break; case 5: // Vertical flip + 90° rotate ctx.transform(0, 1, 1, 0, 0, 0); break; case 6: // 90° rotate ctx.transform(0, 1, -1, 0, height, 0); break; case 7: // Horizontal flip + 90° rotate ctx.transform(0, -1, -1, 0, height, width); break; case 8: // 270° rotate ctx.transform(0, -1, 1, 0, 0, width); break; default: // No transformation break; } }, generateFilename() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `${this.currentUsername}_${year}${month}${day}_${hours}${minutes}${seconds}.jpg`; }, showCaptureLoading() { const btn = document.getElementById('capture-btn'); if (btn) { btn.disabled = true; btn.textContent = '⏳ Processing...'; } }, showCaptureSuccess(blob) { const btn = document.getElementById('capture-btn'); if (btn) { btn.textContent = '✓ Photo Saved!'; setTimeout(() => { btn.disabled = false; btn.textContent = '📷 Take Photo'; }, 1500); } }, showCaptureError(message) { const btn = document.getElementById('capture-btn'); if (btn) { btn.textContent = '❌ Error'; setTimeout(() => { btn.disabled = false; btn.textContent = '📷 Take Photo'; }, 2000); } alert('Failed to capture photo: ' + message); }, async updateRecentPhotos() { if (!this.currentUsername) return; const thumbnailsContainer = document.getElementById('photo-thumbnails'); if (!thumbnailsContainer) return; const recentPhotos = await Storage.getRecentPhotos(this.currentUsername, 5); if (recentPhotos.length === 0) { thumbnailsContainer.innerHTML = '
No photos captured yet
'; return; } let html = ''; for (const photo of recentPhotos) { const url = URL.createObjectURL(photo.blob); const statusClass = photo.status === 'verified' ? 'verified' : photo.status === 'pending' ? 'pending' : photo.status === 'uploading' ? 'uploading' : ''; html += `