···11+# Media Viewer & Downloads
22+33+In-app media viewing and downloading for images and videos embedded in posts.
44+55+## Video Player
66+77+BlueSky videos are HLS streams. The `app.bsky.embed.video#view` embed provides a `playlist` URL (m3u8 manifest) and an optional `thumbnail`.
88+99+### Playback
1010+1111+- Use the native `<video>` element with HLS.js for manifest parsing (Safari handles HLS natively; HLS.js covers Chromium/WebKit in Tauri).
1212+- Inline player replaces the current thumbnail-as-external-link treatment in `EmbedContent`.
1313+- Controls: play/pause, progress scrubber, volume, fullscreen. Use browser-native controls (`controls` attribute) initially — custom controls are a future polish item.
1414+- Muted autoplay is **off** by default. Player shows thumbnail with a centered play button overlay; playback starts on click.
1515+- Respect `aspectRatio` from the embed to size the player container and prevent layout shift.
1616+- `alt` text from the embed is rendered below the player as a caption when present.
1717+1818+### Fullscreen
1919+2020+- Clicking a "fullscreen" control (or double-click on the player) enters native fullscreen via the Fullscreen API.
2121+- `Escape` exits fullscreen (browser default).
2222+2323+## Image Gallery
2424+2525+Clicking any image in an `ImageEmbed` opens a full-window overlay gallery.
2626+2727+### Overlay
2828+2929+- Glass overlay: `surface_container_highest` at 70% opacity + `backdrop-blur: 20px`.
3030+- The selected image is displayed at its natural resolution (`fullsize` URL), constrained to viewport with `object-contain`.
3131+- `Presence` fade-in on open, fade-out on close.
3232+3333+### Navigation
3434+3535+- Left/right arrows (keyboard and on-screen chevron buttons) cycle through images in the post.
3636+- Indicators (dots or `1/4` counter) show position in the set.
3737+- Single-image posts show no navigation controls.
3838+3939+### Caption
4040+4141+Below the image, display:
4242+- **Alt text** from the image embed (primary caption, `body-md`).
4343+- **Post text** (secondary, `label-sm`, `on_surface_variant`, truncated to 2 lines with "show more" expansion).
4444+- **Author handle** linking to their profile.
4545+4646+### Keyboard
4747+4848+| Key | Action |
4949+| ----------------- | ------------------- |
5050+| `Escape` | Close gallery |
5151+| `ArrowLeft` | Previous image |
5252+| `ArrowRight` | Next image |
5353+5454+### Gestures (future)
5555+5656+Pinch-to-zoom and swipe navigation are deferred to a future milestone.
5757+5858+## Downloads
5959+6060+Users can download images and videos to their local filesystem.
6161+6262+### Download Directory
6363+6464+- Default: `~/Downloads`.
6565+- Configurable via a new `download_directory` setting in `app_settings`.
6666+- Settings UI: a path input with a "Browse" button that opens Tauri's directory picker dialog (`dialog.open` with `directory: true`).
6767+- The backend validates that the chosen path exists and is writable before persisting.
6868+6969+### Setting
7070+7171+| Key | Type | Default | Description |
7272+| -------------------- | ------ | -------------- | ---------------------------- |
7373+| `download_directory` | string | `~/Downloads` | Target directory for saves |
7474+7575+### Triggering Downloads
7676+7777+- **Images**: a download button (icon: `i-ri-download-2-line`) appears in the gallery overlay toolbar. Downloads the `fullsize` URL. Also available via right-click context menu on inline images.
7878+- **Videos**: a download button in the video player controls area. Downloads the HLS stream — the backend fetches the m3u8 manifest, resolves the highest-quality variant, downloads all segments, and muxes into a single MP4 file.
7979+8080+### Backend
8181+8282+Video download requires server-side work because HLS streams are segmented:
8383+8484+```rust
8585+// Download a media file (image or video) to the configured download directory.
8686+// For images: direct HTTP fetch of the source URL.
8787+// For videos: fetch m3u8 manifest, download segments, concatenate into MP4.
8888+download_media(url: String, media_type: MediaType, filename: Option<String>) -> DownloadResult
8989+```
9090+9191+- `MediaType`: `Image` or `Video`.
9292+- `DownloadResult`: `{ path: String, bytes: u64 }`.
9393+- Filename: derived from URL path if not provided. Collision handling: append `_1`, `_2`, etc.
9494+- The command should emit progress events (`download-progress`) for large video files so the frontend can show a progress indicator.
9595+9696+### Frontend UX
9797+9898+- Download button shows a brief spinner/progress indicator while active.
9999+- On completion: success toast with the filename and "Open in Finder" action (uses `tauri-plugin-opener`).
100100+- On failure: error toast with a human-readable message ("Couldn't save — check that the download folder exists").
101101+102102+## Tauri Capabilities
103103+104104+The following permissions are needed beyond what `default.json` currently grants:
105105+106106+- `dialog:default` — for the directory picker in settings.
107107+- `fs:default` — for writing downloaded files to disk (scoped to the user's download directory).
108108+109109+## Constraints
110110+111111+- HLS.js is a runtime dependency (~60 KB gzipped). It should be lazy-loaded only when a video embed is in view.
112112+- Video muxing on the backend uses raw segment concatenation for MPEG-TS streams. If the CDN serves fMP4 segments, a lightweight remux step is needed — evaluate `mp4` or `ffmpeg-sidecar` crates at implementation time.
113113+- Downloads are not queued or batched in v1. One download at a time; concurrent downloads are a future enhancement.
+60
docs/tasks/15-media.md
···11+# Milestone 15: Media Viewer & Downloads
22+33+Spec: [media.md](../specs/media.md)
44+55+Depends on: Milestone 03 (Feeds — PostCard, EmbedContent), Milestone 06 (Settings)
66+77+## Steps
88+99+### Backend - `src-tauri/src/media.rs` + `src-tauri/src/commands/media.rs`
1010+1111+- [ ] Add `DownloadDirectory` variant to `SettingsKey` enum, default to `~/Downloads` via `dirs::download_dir()`
1212+- [ ] `get_download_directory()` — resolve current download path (setting or OS default), validate it exists
1313+- [ ] `set_download_directory(path: String)` — validate path is a writable directory, persist to `app_settings`
1414+- [ ] `download_image(url: String, filename: Option<String>)` — HTTP fetch → write to download dir, return `{ path, bytes }`
1515+- [ ] `download_video(url: String, filename: Option<String>)` — fetch m3u8 manifest, resolve best variant, download TS segments, concatenate to MP4, return `{ path, bytes }`
1616+- [ ] Emit `download-progress` events during video download for frontend progress UI
1717+- [ ] Filename collision handling: append `_1`, `_2`, etc. if file already exists
1818+- [ ] Add `dialog:default` and scoped `fs` permissions to `capabilities/default.json`
1919+2020+### Frontend - Video Player (`src/components/feeds/VideoEmbed.tsx`)
2121+2222+- [ ] `VideoEmbed` component: `<video>` element with poster from `thumbnail`, native controls
2323+- [ ] Lazy-load HLS.js — attach to video element only when `playlist` URL is m3u8
2424+- [ ] Click-to-play: show thumbnail + centered play button overlay, start playback on click
2525+- [ ] Respect `aspectRatio` from embed to prevent layout shift
2626+- [ ] Render `alt` text as caption below player when present
2727+- [ ] Replace `ExternalEmbed` fallback in `EmbedContent` switch for `app.bsky.embed.video#view`
2828+- [ ] Download button in player controls area → invoke `download_video` command
2929+3030+### Frontend - Image Gallery (`src/components/feeds/ImageGallery.tsx`)
3131+3232+- [ ] Gallery overlay: glass background (`surface_container_highest` 70% + backdrop-blur 20px)
3333+- [ ] Display `fullsize` image with `object-contain`, constrained to viewport
3434+- [ ] `Presence` fade-in/fade-out transitions
3535+- [ ] Left/right navigation arrows + position indicator for multi-image posts
3636+- [ ] Keyboard: `Escape` close, `ArrowLeft`/`ArrowRight` navigate
3737+- [ ] Caption panel: alt text (`body-md`), post text truncated to 2 lines with expand, author handle as link
3838+- [ ] Download button in gallery toolbar → invoke `download_image` command
3939+- [ ] Wire `ImageEmbed` click handler to open gallery at the clicked image index
4040+4141+### Frontend - Download UX
4242+4343+- [ ] Download button spinner/progress indicator while active
4444+- [ ] Success toast: filename + "Open in Finder" action (via `tauri-plugin-opener`)
4545+- [ ] Error toast: human-readable failure message
4646+- [ ] Right-click context menu on inline images with "Save image" option
4747+4848+### Frontend - Settings Integration
4949+5050+- [ ] Add "Downloads" section to Settings view between "Data" and "Danger Zone"
5151+- [ ] Path display + "Browse" button using Tauri `dialog.open({ directory: true })`
5252+- [ ] "Reset to default" link to restore `~/Downloads`
5353+5454+### Parking Lot
5555+5656+- [ ] Custom video player controls (scrubber, volume, speed)
5757+- [ ] Pinch-to-zoom and swipe gestures in gallery
5858+- [ ] Download queue with concurrent downloads
5959+- [ ] Batch download (all images in a post)
6060+- [ ] Save to custom album/folder per account