--- phase: 03-images plan: 03 type: execute wave: 2 depends_on: ["03-02"] files_modified: - src/lib/components/CameraCapture.svelte autonomous: true must_haves: truths: - "User can open camera modal" - "User can see live camera preview" - "User can capture a photo" - "User can preview captured photo before confirming" - "User can retake if not satisfied" - "Camera stream stops when modal closes" artifacts: - path: "src/lib/components/CameraCapture.svelte" provides: "Camera capture modal with preview and confirm" min_lines: 100 key_links: - from: "src/lib/components/CameraCapture.svelte" to: "navigator.mediaDevices.getUserMedia" via: "browser API call" pattern: "getUserMedia" - from: "src/lib/components/CameraCapture.svelte" to: "canvas.toBlob" via: "photo capture" pattern: "toBlob" --- Implement camera capture component for mobile photo taking with preview and confirm flow. Purpose: Enables users to take photos directly in the app, critical for the core use case of photographing paper notes. Direct camera access feels more native than file picker on mobile. Output: - CameraCapture modal component with live preview - Capture, preview, retake, confirm flow - Proper stream cleanup to prevent battery drain @/home/tho/.claude/get-shit-done/workflows/execute-plan.md @/home/tho/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/phases/03-images/03-RESEARCH.md @.planning/phases/03-images/03-01-SUMMARY.md Task 1: CameraCapture component with getUserMedia src/lib/components/CameraCapture.svelte Create CameraCapture.svelte - a modal for camera capture: Props: - entryId: string (required) - onClose: () => void (required - parent controls open state) - onCapture: (imageId: string) => void (optional callback after upload) State: - stream: MediaStream | null ($state) - capturedBlob: Blob | null ($state) - capturedPreviewUrl: string | null ($state) - error: string | null ($state) - isUploading: boolean ($state) - facingMode: 'environment' | 'user' ($state) - default 'environment' (back camera) Element refs: - videoElement: HTMLVideoElement - canvasElement: HTMLCanvasElement Functions: 1. startCamera(): - Check navigator.mediaDevices?.getUserMedia exists, set error if not - Stop existing stream if any - Try getUserMedia with: video: { facingMode, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false - Catch errors: - NotAllowedError: "Camera permission denied" - NotFoundError: "No camera found" - Other: "Could not access camera" - Set stream, assign to videoElement.srcObject, call play() 2. stopCamera(): - stream?.getTracks().forEach(track => track.stop()) - stream = null - videoElement.srcObject = null 3. capturePhoto(): - Get 2D context from canvas - Set canvas dimensions to video dimensions - drawImage(videoElement, 0, 0) - canvas.toBlob() with 'image/jpeg', 0.9 quality - Store blob, create preview URL - Stop camera (save battery while reviewing) 4. retake(): - Revoke preview URL - Clear capturedBlob and capturedPreviewUrl - startCamera() again 5. confirmAndUpload(): - isUploading = true - Create FormData with blob as 'image' (filename: 'camera-capture.jpg') - Add entryId - fetch('?/uploadImage', { method: 'POST', body: formData }) - Handle response - Call onCapture, onClose - Revoke preview URL, clean up 6. switchCamera(): - Toggle facingMode - Restart camera with new facing mode 7. handleClose(): - stopCamera() - Revoke any preview URL - onClose() Lifecycle: - $effect: startCamera() on mount - $effect: cleanup on unmount (stopCamera) Template structure: ```
{#if error}

{error}

{:else if capturedPreviewUrl} Captured {:else}
``` CRITICAL: Always stop camera stream when component closes or captures. Camera indicator should not stay on. Styling: - Full screen black background (typical camera UI) - Large touch targets for capture button (min 64px) - Video fills available space while maintaining aspect ratio
Run `npm run check` - TypeScript compiles. File exists and has key functions: `grep -c "getUserMedia\|capturePhoto\|stopCamera" src/lib/components/CameraCapture.svelte` CameraCapture modal with live preview renders. Capture creates blob from canvas. Preview shows captured image with retake/confirm options. Camera stream properly stopped on close and after capture.
Task 2: Feature detection and graceful fallback src/lib/components/CameraCapture.svelte Enhance CameraCapture with proper feature detection and error handling: 1. Add hasCameraSupport check: ```typescript const hasCameraSupport = typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia; ``` 2. If no camera support, show friendly message immediately (don't try to call getUserMedia): "Camera not supported in this browser. Use the file upload option instead." 3. Add isSecureContext check: - Camera API requires HTTPS (except localhost) - Show message if not secure: "Camera requires HTTPS connection" 4. Handle permission denied gracefully: - Clear error message explaining how to enable camera - "Camera access denied. Check browser settings to allow camera." 5. Handle no camera found: - "No camera found on this device. Use file upload instead." 6. Add timeout for camera start: - If camera doesn't start within 10 seconds, show error - Prevents infinite loading state 7. Test on desktop without webcam - should show "No camera found" not crash The error states should have a close button that calls onClose(). Run `npm run check` - TypeScript compiles. Grep for error handling: `grep -c "NotAllowedError\|NotFoundError\|hasCameraSupport" src/lib/components/CameraCapture.svelte` CameraCapture gracefully handles missing camera, permission denied, insecure context. Error messages are user-friendly with action guidance.
1. `npm run check` passes 2. `npm run dev` - server starts 3. Component file exists: `ls src/lib/components/CameraCapture.svelte` 4. Has required functions: grep for getUserMedia, stopCamera, capturePhoto, toBlob 5. Has error handling: grep for NotAllowedError, NotFoundError - CameraCapture opens with live camera preview - Capture button takes photo and shows preview - Retake returns to live camera - Confirm uploads the photo - Camera stream stops when modal closes - Graceful error handling for permission denied, no camera, unsupported browser After completion, create `.planning/phases/03-images/03-03-SUMMARY.md`