Files
taskplaner/.planning/phases/03-images/03-03-PLAN.md
Thomas Richter b09ac9013b docs(03): create phase plans with research
Phase 03: Images
- 4 plans across 3 waves
- Sharp for thumbnails, native browser APIs for camera
- svelte-lightbox for fullscreen viewing
- Fix: 03-03 depends on 03-02 (needs uploadImage action)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 15:18:04 +01:00

8.1 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
03-images 03 execute 2
03-02
src/lib/components/CameraCapture.svelte
true
truths artifacts key_links
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
path provides min_lines
src/lib/components/CameraCapture.svelte Camera capture modal with preview and confirm 100
from to via pattern
src/lib/components/CameraCapture.svelte navigator.mediaDevices.getUserMedia browser API call getUserMedia
from to via pattern
src/lib/components/CameraCapture.svelte canvas.toBlob photo capture 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

<execution_context> @/home/tho/.claude/get-shit-done/workflows/execute-plan.md @/home/tho/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

<div class="fixed inset-0 z-50 bg-black flex flex-col">
  <!-- Header with close button and switch camera button -->
  <div class="flex justify-between p-4">
    <button close>X</button>
    <button switchCamera>Flip</button>
  </div>

  <!-- Main content area -->
  <div class="flex-1 flex items-center justify-center">
    {#if error}
      <p class="text-white">{error}</p>
      <button onclick={onClose}>Close</button>
    {:else if capturedPreviewUrl}
      <!-- Preview captured photo -->
      <img src={capturedPreviewUrl} alt="Captured" class="max-h-full max-w-full object-contain" />
    {:else}
      <!-- Live camera view -->
      <video bind:this={videoElement} autoplay playsinline muted class="max-h-full max-w-full object-contain" />
    {/if}
  </div>

  <!-- Hidden canvas for capture -->
  <canvas bind:this={canvasElement} class="hidden" />

  <!-- Action buttons -->
  <div class="p-4 flex justify-center gap-4">
    {#if capturedPreviewUrl}
      <button retake disabled={isUploading}>Retake</button>
      <button confirm disabled={isUploading}>
        {isUploading ? 'Uploading...' : 'Use Photo'}
      </button>
    {:else if !error}
      <button capture class="w-16 h-16 rounded-full bg-white">
        <!-- Capture button - large circle -->
      </button>
    {/if}
  </div>
</div>

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:

    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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/03-images/03-03-SUMMARY.md`