Skip to content

Commit f381686

Browse files
authored
docs: add spec, plan and tasks for feature 029 camera capture (#4209)
1 parent 4138667 commit f381686

5 files changed

Lines changed: 435 additions & 1 deletion

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Feature Plan 029 – Camera Capture
2+
3+
_Linked specification:_ `docs/specs/4-architecture/features/029-camera-capture/spec.md`
4+
_Status:_ Implemented
5+
_Last updated:_ 2026-03-18
6+
7+
> Guardrail: Keep this plan traceable back to the governing spec. Reference FR/NFR/Scenario IDs from `spec.md` where relevant, log any new high- or medium-impact questions in [docs/specs/4-architecture/open-questions.md](../../open-questions.md), and assume clarifications are resolved only when the spec's normative sections (requirements/NFR/behaviour/telemetry) and, where applicable, ADRs under `docs/specs/5-decisions/` have been updated.
8+
9+
## Key Implementation Decisions (from Resolved Questions)
10+
11+
**Q-029-01 (resolved):** Photos captured at the root albums view are uploaded to Unsorted (null album ID). This mirrors the existing upload pipeline's behaviour when no album is in context — no album picker dialog is shown.
12+
13+
**NFR-029-01 (no open question needed):** The `MediaDevices.getUserMedia` guard was added directly to `startCamera()`. A secure-context failure is surfaced as a human-readable `errorMessage` string; no JS exception escapes.
14+
15+
## Architecture Overview
16+
17+
This feature is **frontend-only**. No backend or API changes are required: the captured JPEG is fed directly into the existing Pinia-based upload queue (`list_upload_files`) and opened via `UploadPanel`, identical to a manually selected file.
18+
19+
```
20+
CameraCapture.vue
21+
├── navigator.mediaDevices.getUserMedia() (browser MediaDevices API)
22+
├── <video> element → <canvas>.toBlob() → File object
23+
└── list_upload_files.push({ file, status: "waiting" })
24+
└── UploadPanel (existing component — unchanged)
25+
```
26+
27+
## Implementation Increments
28+
29+
### I1 – Store & Toggle Wiring
30+
- Add `is_camera_capture_visible: false` to `ModalsState.ts` state.
31+
- Add `toggleCameraCapture()` to `galleryModals.ts` composable.
32+
- **Refs:** FR-029-01
33+
34+
### I2 – "Take Photo" Menu Entry (Album View)
35+
- Add `toggleCameraCapture` to `Callbacks` type in `contextMenuAlbumAdd.ts`.
36+
- Add "Take Photo" menu item (`pi pi-camera`, label `gallery.menus.take_photo`) between "Upload Photo" and the first divider.
37+
- Wire into `AlbumHeader.vue`.
38+
- **Refs:** FR-029-01, S-029-01
39+
40+
### I3 – "Take Photo" Menu Entry (Root Albums View)
41+
- Apply identical changes to `contextMenuAlbumsAdd.ts` and `AlbumsHeader.vue`.
42+
- **Refs:** FR-029-01, S-029-03
43+
44+
### I4 – CameraCapture.vue Component
45+
- New file: `resources/js/components/modals/CameraCapture.vue`.
46+
- PrimeVue `Dialog` (`dismissable-mask`, `border-none`).
47+
- States: loading spinner → live video → still preview → error message.
48+
- `startCamera()` using `.then()/.catch()` (no `async/await`; NFR-029-04).
49+
- `capture()` uses `canvas.toBlob()` with a `function` callback.
50+
- `upload()` constructs `File` named `photo_<ISO timestamp>.jpg` and pushes to `list_upload_files`.
51+
- `watch(is_camera_capture_visible)` drives both `startCamera()` and `stopCamera()` — no `@hide` handler needed (prevents double-stop).
52+
- **Refs:** FR-029-02 through FR-029-07, NFR-029-01, NFR-029-02, NFR-029-04
53+
54+
### I5 – Mount CameraCapture in Gallery Views
55+
- Add `<CameraCapture v-if="…can_upload…" />` to `Album.vue` and `Albums.vue`.
56+
- **Refs:** FR-029-01, S-029-02, S-029-04
57+
58+
### I6 – Translations
59+
- `lang/en/gallery.php`: add `'take_photo' => 'Take Photo'` to menus section.
60+
- `lang/en/dialogs.php`: add `'camera'` section with `title`, `capture`, `retake`, `upload` keys.
61+
- **Refs:** FR-029-01, FR-029-02, FR-029-03, FR-029-04, FR-029-05
62+
63+
### I7 – Server Permissions-Policy Header
64+
- `config/secure-headers.php`: set `camera.self = true` so the server emits `Permissions-Policy: camera=(self)`.
65+
- **Refs:** NFR-029-03
66+
67+
### I8 – Mobile Layout
68+
- `CameraCapture.vue`: `max-h-[60vh]` on `<video>` and `<img>`; `max-h-screen overflow-y-auto` on outer container.
69+
- `UploadPanel.vue`: outer container `w-screen max-w-md max-h-screen flex flex-col overflow-hidden`; content area `flex-1 overflow-y-auto`; button row `flex-shrink-0`; `ScrollPanel` width `w-full`.
70+
- **Refs:** NFR-029-02
71+
72+
## Quality Gates
73+
74+
```bash
75+
npm run format # Prettier
76+
npm run check # vue-tsc
77+
```
78+
79+
No PHP changes → PHP quality gates not required for this feature.
80+
81+
## Dependencies
82+
83+
- Existing `UploadPanel.vue` and `list_upload_files` store — no changes needed to the upload pipeline itself.
84+
- PrimeVue `Dialog`, `Button` — already available.
85+
- `Permissions-Policy` header — requires `config/secure-headers.php` change (I7).
86+
87+
---
88+
89+
*Last updated: 2026-03-18*
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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

Comments
 (0)