|
| 1 | +# Feature 029 – Camera Capture |
| 2 | + |
| 3 | +| Field | Value | |
| 4 | +|-------|-------| |
| 5 | +| Status | Implemented | |
| 6 | +| Last updated | 2026-03-18 | |
| 7 | +| Owners | mitpjones | |
| 8 | +| Linked plan | `docs/specs/4-architecture/features/029-camera-capture/plan.md` | |
| 9 | +| Linked tasks | `docs/specs/4-architecture/features/029-camera-capture/tasks.md` | |
| 10 | +| Roadmap entry | #029 | |
| 11 | + |
| 12 | +> Guardrail: This specification is the single normative source of truth for the feature. Track high- and medium-impact questions in [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md), encode resolved answers directly in the Requirements/NFR/Behaviour/UI/Telemetry sections below, and use ADRs under `docs/specs/5-decisions/` for architecturally significant clarifications. |
| 13 | +
|
| 14 | +## Overview |
| 15 | + |
| 16 | +Users want to take a photo using their device camera directly from within Lychee and have it uploaded to the current album (or to Unsorted when at the root gallery view). This removes the need to use a separate camera app and then upload the file manually. The feature affects the gallery UI (album view and root albums view) and the existing upload pipeline. |
| 17 | + |
| 18 | +## Goals |
| 19 | + |
| 20 | +- Add a "Take Photo" option to the "+" add menu in both the album view and the root albums view. |
| 21 | +- Open a modal that streams the device camera, allows capture, shows a preview, and on confirmation feeds the JPEG into the existing `UploadPanel` upload pipeline. |
| 22 | +- Photos captured inside an album upload to that album; photos captured at the root view upload to Unsorted (null album ID). |
| 23 | +- Show a clear error when the browser environment does not support camera access (non-secure context). |
| 24 | + |
| 25 | +## Non-Goals |
| 26 | + |
| 27 | +- Video recording. |
| 28 | +- Selecting from existing device photos (that is covered by the existing file upload). |
| 29 | +- Camera access on browsers that do not support `MediaDevices.getUserMedia`. |
| 30 | +- Album picker flow after capture at root level (deferred; see Q-029-01 resolution). |
| 31 | + |
| 32 | +## Functional Requirements |
| 33 | + |
| 34 | +| ID | Requirement | Success path | Validation path | Failure path | Telemetry & traces | Source | |
| 35 | +|----|-------------|--------------|-----------------|--------------|--------------------|---------| |
| 36 | +| FR-029-01 | "Take Photo" menu item appears in the "+" add menu when the user has `can_upload` rights, in both album and root views. | Menu item is visible and labelled correctly. | Only shown when `can_upload === true`. | Hidden when rights absent. | — | User request | |
| 37 | +| FR-029-02 | Tapping "Take Photo" opens the camera capture modal and starts the rear-facing camera stream. | Live video preview visible; capture button enabled once stream is ready. | `facingMode: "environment"` requested; spinner shown while stream initialises. | Error message shown if `getUserMedia` fails or is denied. | — | User request | |
| 38 | +| FR-029-03 | Tapping "Capture" freezes the current frame as a JPEG preview. | Still image shown; "Retake" and "Upload" buttons appear. | Canvas draws at native video resolution. | If canvas/blob creation fails silently, nothing is pushed. | — | User request | |
| 39 | +| FR-029-04 | Tapping "Upload" adds the JPEG to the upload queue and opens `UploadPanel`. | File named `photo_<ISO timestamp>.jpg` pushed to `list_upload_files`; `UploadPanel` opens. | Album ID taken from current route param; null at root (Unsorted). | — | — | User request; Q-029-01 resolved | |
| 40 | +| FR-029-05 | Tapping "Retake" discards the preview, stops the stream, and restarts the camera. | Live preview resumes. | — | Camera re-request may be denied by browser. | — | User request | |
| 41 | +| FR-029-06 | Closing the modal stops the camera stream and resets all state. | Stream tracks stopped; video/blob/error state cleared. | Watch on `is_camera_capture_visible` drives cleanup. | — | — | User request | |
| 42 | +| FR-029-07 | When `MediaDevices.getUserMedia` is unavailable (non-secure context), a human-readable error is shown immediately. | Error message instructs user to use HTTPS or localhost. | Checked before `getUserMedia` call via `navigator.mediaDevices?.getUserMedia`. | — | — | NFR-029-01 | |
| 43 | + |
| 44 | +## Non-Functional Requirements |
| 45 | + |
| 46 | +| ID | Requirement | Driver | Measurement | Dependencies | Source | |
| 47 | +|----|-------------|--------|-------------|--------------|--------| |
| 48 | +| NFR-029-01 | Camera access requires a secure context (HTTPS or `localhost`). The feature must degrade gracefully — showing an error — rather than throwing a JS exception. | Browser security model (`MediaDevices` only available in secure contexts). | Manual test on HTTP non-localhost origin shows error message, not JS exception. | `navigator.mediaDevices?.getUserMedia` guard in `CameraCapture.vue`. | MDN Secure Contexts | |
| 49 | +| NFR-029-02 | The capture modal must be usable on small portrait and landscape mobile screens. | Primary use case is phone camera. | Buttons remain visible and reachable without scrolling on a 375×667 viewport in both orientations. | `max-h-[60vh]` on video/image; `max-h-screen overflow-y-auto` on container. | User report | |
| 50 | +| NFR-029-03 | The server `Permissions-Policy` header must allow camera access from the same origin. | FrankenPHP/Laravel secure-headers middleware blocks camera by default. | `camera=(self)` present in response headers on the gallery page. | `config/secure-headers.php` `camera.self = true`. | Browser Permissions Policy spec | |
| 51 | +| NFR-029-04 | Frontend code must follow project conventions: `.then()` over `async/await`, `function` declarations over arrow functions. | Coding conventions in `docs/specs/3-reference/coding-conventions.md`. | `npm run check` passes; no `async` keyword in `CameraCapture.vue`. | — | Coding conventions doc | |
| 52 | + |
| 53 | +## UI / Interaction Mock-ups |
| 54 | + |
| 55 | +### "+" Add Menu (album view) |
| 56 | + |
| 57 | +``` |
| 58 | +┌─────────────────────────┐ |
| 59 | +│ ↑ Upload Photo │ |
| 60 | +│ 📷 Take Photo │ ← new |
| 61 | +│ ─────────────────────── │ |
| 62 | +│ 🔗 Import from Link │ |
| 63 | +│ 📦 Import from Dropbox │ |
| 64 | +│ 🖥 Import from Server │ |
| 65 | +│ ─────────────────────── │ |
| 66 | +│ 📁 New Album │ |
| 67 | +│ 🧭 Upload track │ |
| 68 | +└─────────────────────────┘ |
| 69 | +``` |
| 70 | + |
| 71 | +### Camera Capture Modal — live view |
| 72 | + |
| 73 | +``` |
| 74 | +┌───────────────────────────┐ |
| 75 | +│ Take a Photo │ |
| 76 | +│ ┌───────────────────────┐ │ |
| 77 | +│ │ │ │ |
| 78 | +│ │ [ live camera feed ]│ │ |
| 79 | +│ │ │ │ |
| 80 | +│ └───────────────────────┘ │ |
| 81 | +│ [ Capture 📷 ] │ |
| 82 | +└───────────────────────────┘ |
| 83 | +``` |
| 84 | + |
| 85 | +### Camera Capture Modal — after capture |
| 86 | + |
| 87 | +``` |
| 88 | +┌───────────────────────────┐ |
| 89 | +│ Take a Photo │ |
| 90 | +│ ┌───────────────────────┐ │ |
| 91 | +│ │ │ │ |
| 92 | +│ │ [ still preview ] │ │ |
| 93 | +│ │ │ │ |
| 94 | +│ └───────────────────────┘ │ |
| 95 | +│ [ Retake 🔄 ] [ Upload ↑]│ |
| 96 | +└───────────────────────────┘ |
| 97 | +``` |
| 98 | + |
| 99 | +## Branch & Scenario Matrix |
| 100 | + |
| 101 | +| Scenario ID | Description / Expected outcome | |
| 102 | +|-------------|-------------------------------| |
| 103 | +| S-029-01 | User opens album, taps "+", selects "Take Photo" — modal opens, camera starts. | |
| 104 | +| S-029-02 | User captures photo in album — JPEG uploaded to that album. | |
| 105 | +| S-029-03 | User opens root albums view, taps "+", selects "Take Photo" — modal opens, camera starts. | |
| 106 | +| S-029-04 | User captures photo at root — JPEG uploaded to Unsorted. | |
| 107 | +| S-029-05 | User taps "Retake" — stream restarts, preview discarded. | |
| 108 | +| S-029-06 | User dismisses modal — stream stopped, state reset. | |
| 109 | +| S-029-07 | User on HTTP non-localhost — error message shown, no JS exception. | |
| 110 | +| S-029-08 | User denies camera permission — browser error message shown in modal. | |
| 111 | + |
| 112 | +## Test Strategy |
| 113 | + |
| 114 | +- **UI (JS):** Component tests for `CameraCapture.vue` covering: modal visibility toggle, error display on missing `getUserMedia`, post-capture state (blob/dataUrl set, stream stopped), upload action pushes correct File to store. `npm run check`. |
| 115 | +- **Core / REST / CLI:** No backend changes; existing upload pipeline tests cover the upload path. |
| 116 | + |
| 117 | +## Interface & Contract Catalogue |
| 118 | + |
| 119 | +### UI States |
| 120 | + |
| 121 | +| ID | State | Trigger / Expected outcome | |
| 122 | +|----|-------|---------------------------| |
| 123 | +| UI-029-01 | Idle (modal closed) | `is_camera_capture_visible === false`; stream null. | |
| 124 | +| UI-029-02 | Loading camera | Modal open; `cameraReady === false`; spinner shown. | |
| 125 | +| UI-029-03 | Live preview | `cameraReady === true`; Capture button enabled. | |
| 126 | +| UI-029-04 | Preview captured | `capturedBlob` set; Retake + Upload buttons shown. | |
| 127 | +| UI-029-05 | Error | `errorMessage` non-empty; no video shown. | |
| 128 | + |
| 129 | +## Telemetry & Observability |
| 130 | + |
| 131 | +No telemetry events introduced. Camera permission errors are surfaced in the UI only. |
| 132 | + |
| 133 | +## Documentation Deliverables |
| 134 | + |
| 135 | +- `docs/specs/4-architecture/features/029-camera-capture/spec.md` (this file) |
| 136 | +- `docs/specs/4-architecture/features/029-camera-capture/plan.md` |
| 137 | +- `docs/specs/4-architecture/features/029-camera-capture/tasks.md` |
| 138 | + |
| 139 | +## Spec DSL |
| 140 | + |
| 141 | +``` |
| 142 | +ui_states: |
| 143 | + - id: UI-029-01 |
| 144 | + description: Modal closed, stream null |
| 145 | + - id: UI-029-02 |
| 146 | + description: Loading camera, spinner shown |
| 147 | + - id: UI-029-03 |
| 148 | + description: Live camera preview, capture enabled |
| 149 | + - id: UI-029-04 |
| 150 | + description: Still preview shown, retake/upload available |
| 151 | + - id: UI-029-05 |
| 152 | + description: Error state, message shown |
| 153 | +``` |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +*Last updated: 2026-03-18* |
0 commit comments