···5454Build a `MessageBloc` with events: `MessagesRequested`, `MessagesPageLoaded`,
5555`MessageSent`, `MessageDeleted`, `ConvoMarkedRead`.
56565757+## Media Playback & Download
5858+5959+Currently images open in an external browser and videos launch an external app
6060+via `url_launcher`. This milestone adds in-app media viewing and the ability to
6161+save media to the device gallery.
6262+6363+### Packages
6464+6565+| Package | Purpose |
6666+| -------------------- | --------------------------------------------------------------- |
6767+| `photo_view` | Pinch-to-zoom and pan for full-screen images |
6868+| `video_player` | Flutter's official video playback plugin (HLS support built-in) |
6969+| `chewie` | Material-styled controls wrapper around `video_player` |
7070+| `dio` | HTTP downloads with progress callbacks |
7171+| `gal` | Save images and videos to the device gallery |
7272+| `permission_handler` | Request photo-library / storage write permissions |
7373+7474+### Image Viewer
7575+7676+Tapping an image in a post opens a full-screen `ImageViewerScreen`. The screen
7777+is a `PageView` so multi-image posts are swipeable. Each page contains a
7878+`PhotoView` widget wrapping an `Image.network` of the `fullsize` URL. A hero
7979+animation on the thumbnail provides a smooth transition.
8080+8181+The viewer has a transparent app bar with a close button, a download button, and
8282+a share button. Swiping down dismisses the viewer. The current page indicator
8383+appears at the bottom for multi-image posts.
8484+8585+Alt text, when present, is shown in a semi-transparent bar at the bottom of
8686+each page.
8787+8888+### Video Player
8989+9090+Tapping a video embed opens a `VideoPlayerScreen`. The player uses `chewie`
9191+wrapping Flutter's `VideoPlayerController.networkUrl` pointed at the HLS
9292+`playlist` URL. Controls include play/pause, seek bar, elapsed/total time,
9393+fullscreen toggle, and a mute button.
9494+9595+The video thumbnail is shown as a placeholder until the player initialises. If
9696+the embed has an `aspectRatio`, the player container uses it; otherwise it
9797+defaults to 16:9. The player disposes its controller on screen pop.
9898+9999+For GIF-style videos (`presentation: "gif"`), the player auto-plays in a loop
100100+with controls hidden and audio muted.
101101+102102+### Downloading Media
103103+104104+A download button appears in the image viewer toolbar and the video player
105105+toolbar. The download flow:
106106+107107+1. Check and request write permission via `permission_handler` (photo library
108108+ on iOS, storage or media-store on Android).
109109+2. Download the file using `dio` with a progress callback driving a circular
110110+ progress indicator on the button.
111111+3. Save the file to the device gallery via `gal`.
112112+4. Show a snackbar confirming success or displaying the error.
113113+114114+For images, the download URL is the `fullsize` URL. For videos, download the
115115+highest-quality variant from the HLS playlist. Parse the `.m3u8` manifest to
116116+find the highest-bandwidth variant URL, then download that MP4 stream.
117117+118118+Long-press on an image thumbnail in a post (without entering the viewer) should
119119+show a context menu with "Save image" and "Share" options.
120120+121121+### Permissions
122122+123123+| Platform | Permission | When Requested |
124124+| ----------- | --------------------------------------- | ---------------------- |
125125+| iOS | `NSPhotoLibraryAddUsageDescription` | First download attempt |
126126+| Android 13+ | `READ_MEDIA_IMAGES`, `READ_MEDIA_VIDEO` | First download attempt |
127127+| Android <13 | `WRITE_EXTERNAL_STORAGE` | First download attempt |
128128+129129+Declare permissions in `AndroidManifest.xml` and `Info.plist`. The app only
130130+requests permission at the moment of download, not on launch.
131131+57132## Account Switching
5813359134Support multiple authenticated accounts with full data isolation. The
+21-4
docs/tasks/phase-4.md
···1414- [x] Mute / unmute conversations
1515- [x] Mark conversation as read via `chat.bsky.convo.updateRead`
16161717-## M13 — Account Switching
1717+## M13 — Media Playback & Download
1818+1919+- [ ] Add `photo_view`, `video_player`, `chewie`, `dio`, `gal`, `permission_handler` to `pubspec.yaml`
2020+- [ ] `ImageViewerScreen` — full-screen `PageView` of `PhotoView` widgets loading `fullsize` URLs with hero animation from thumbnail
2121+- [ ] Page indicator for multi-image posts; alt text bar at the bottom of each page
2222+- [ ] Swipe-down-to-dismiss gesture on image viewer
2323+- [ ] Download button in image viewer toolbar — request permission, download via `dio` with progress indicator, save via `gal`, show snackbar result
2424+- [ ] Share button in image viewer toolbar via `share_plus`
2525+- [ ] Long-press context menu on image thumbnails in post cards — "Save image" and "Share" options
2626+- [ ] `VideoPlayerScreen` — `chewie` wrapping `VideoPlayerController.networkUrl` with HLS `playlist` URL
2727+- [ ] Video player uses embed `aspectRatio` when available, defaults to 16:9
2828+- [ ] Video thumbnail as placeholder until player initialises; controller disposed on screen pop
2929+- [ ] GIF-presentation mode — auto-play, loop, muted, controls hidden when `presentation` is `"gif"`
3030+- [ ] Download button in video player toolbar — parse `.m3u8` for highest-bandwidth variant URL, download MP4 via `dio` with progress, save via `gal`
3131+- [ ] Declare `NSPhotoLibraryAddUsageDescription` in `Info.plist` and storage permissions in `AndroidManifest.xml`
3232+- [ ] Replace `_launchExternal` calls for image/video embeds in `PostCard` with navigation to the new viewer screens
3333+3434+## M14 — Account Switching
18351936- [x] `AccountSwitcherCubit` exposing account list and active DID
2037- [ ] Account switcher bottom sheet UI — list accounts with avatars and handles
···2542- [ ] "Add Account" button triggers OAuth flow, inserts new `accounts` row
2643- [ ] Silent token refresh on account switch; navigate to login on failure
27442828-## M14 — Offline Reading & Network Resilience
4545+## M15 — Offline Reading & Network Resilience
29463047- [x] `ConnectivityCubit` via **connectivity_plus** — expose network state stream
3148- [ ] Cache last-fetched feed page as serialised JSON in Drift
···3451- [ ] Disable network-dependent actions (compose, like, repost, follow) when offline with tooltip
3552- [ ] Notifications and DM screens show "No connection" empty state when offline with no cache
36533737-## M15 — Jump to Profile
5454+## M16 — Jump to Profile
38553956- [ ] Floating action button on search screen
4057- [ ] Handle input dialog with autocomplete via `searchActorsTypeahead`
4158- [ ] Navigate to profile screen on selection or enter
4259- [ ] Update bottom navigation to include Notifications and Messages tabs (5-tab layout)
43604444-## M16 — Labelers & Content Moderation
6161+## M17 — Labelers & Content Moderation
45624663- [x] Fetch user's labeler subscriptions from preferences via `app.bsky.actor.getPreferences` (`labelersPref`)
4764- [ ] Include subscribed labeler DIDs in `atproto-accept-labelers` header on all XRPC requests