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>
264 lines
8.1 KiB
Markdown
264 lines
8.1 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/tho/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/phases/03-images/03-RESEARCH.md
|
|
@.planning/phases/03-images/03-01-SUMMARY.md
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: CameraCapture component with getUserMedia</name>
|
|
<files>
|
|
src/lib/components/CameraCapture.svelte
|
|
</files>
|
|
<action>
|
|
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
|
|
</action>
|
|
<verify>
|
|
Run `npm run check` - TypeScript compiles.
|
|
File exists and has key functions: `grep -c "getUserMedia\|capturePhoto\|stopCamera" src/lib/components/CameraCapture.svelte`
|
|
</verify>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Feature detection and graceful fallback</name>
|
|
<files>
|
|
src/lib/components/CameraCapture.svelte
|
|
</files>
|
|
<action>
|
|
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().
|
|
</action>
|
|
<verify>
|
|
Run `npm run check` - TypeScript compiles.
|
|
Grep for error handling: `grep -c "NotAllowedError\|NotFoundError\|hasCameraSupport" src/lib/components/CameraCapture.svelte`
|
|
</verify>
|
|
<done>
|
|
CameraCapture gracefully handles missing camera, permission denied, insecure context.
|
|
Error messages are user-friendly with action guidance.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/03-images/03-03-SUMMARY.md`
|
|
</output>
|