aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/specs')
-rw-r--r--docs/superpowers/specs/2026-10-22-mygram-droid-design.md167
1 files changed, 167 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-10-22-mygram-droid-design.md b/docs/superpowers/specs/2026-10-22-mygram-droid-design.md
new file mode 100644
index 0000000..958f0a6
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-22-mygram-droid-design.md
@@ -0,0 +1,167 @@
+# mygram-droid design
+
+Date: 2026-10-22
+
+Native Android TV client for a self-hosted Foldergram instance. Lean-back
+browsing of folders, photos and (mostly vertical) videos with the remote,
+plus shared likes.
+
+## Target
+
+- Chromecast with Google TV HD: Google TV 14 (API 34), 32-bit ARM,
+ about 1.4 GB RAM.
+- Foldergram on the LAN, plain HTTP, default port 4141. The app allows
+ cleartext traffic.
+- The instance runs in viewer-password mode: every route, including
+ `/thumbnails/...` and `/previews/...`, needs a session.
+
+## Stack
+
+- Java, minSdk 34, one `app` module.
+- Built in a throwaway Docker container (JDK 17, SDK 34), same pattern as
+ the `tv-debloat` launcher.
+- Dependencies: Glide (images) and Media3 ExoPlayer (video). Everything
+ else from the platform: `HttpURLConnection`, `org.json`,
+ `SharedPreferences`, `RenderEffect`, `MediaMetadataRetriever`.
+
+## Screens
+
+### Instance picker (first launch, or "Change instance" in Settings)
+
+- Scans the TV's /24 subnet on port 4141: `GET /api/health` on every
+ address, about 32 in parallel, short timeout, about 3 to 5 s total.
+- A host is accepted only if the reply is JSON with `ok: true` and a
+ `storage` object.
+- Each result shows its reverse-DNS name when available, else the IP.
+- Extra entries: "Enter URL manually", "Rescan".
+- On selection: `GET /api/auth/status`. If `accessMode` is `password`,
+ prompt for the password and `POST /api/auth/login`. If `public`, no
+ password. Save URL and password, then open Home.
+- Limits: misses instances on another port or behind a reverse proxy;
+ manual entry covers those.
+
+### Top menu
+
+`Home · Reels · Liked · Folders · Settings`, always visible except in the
+viewer. Up from the content reaches it.
+
+### Home
+
+- Background: a random image from `GET /api/feed?mode=random`, blurred and
+ scaled up, crossfading every N seconds (Settings, default 15 s).
+- One horizontal row per folder from `GET /api/folders`, ordered by
+ `latestImageMtimeMs` descending, rows loaded lazily on scroll.
+- Each row: about 10 random items from that folder, then a **Continue**
+ tile.
+- OK on an item: viewer at that item, continuing through the folder's
+ shuffled list. OK on Continue: folder grid.
+
+### Folders
+
+Grid of all folders as avatar tiles (`avatarUrl`). OK: folder grid.
+
+### Folder grid
+
+- Fetches every page of `GET /api/folders/:slug/images` (60 per request)
+ and shuffles locally once per open. The API has no random mode for
+ folders.
+- OK: viewer at that item, continuing in grid order.
+
+### Liked
+
+- Grid from `GET /api/likes`, newest like first.
+- OK: viewer in that order.
+- Posts unliked in the viewer are gone from the grid on return.
+
+### Reels
+
+Opens the viewer directly on `GET /api/reels?mode=random&seed=<random>`,
+videos only, endless, paged with the same seed.
+
+### Viewer (fullscreen)
+
+- Item centered at its own aspect ratio over a blurred, scaled-up backdrop
+ that fills the side bars.
+- Photo backdrop: the photo's own preview, decoded small (about 200 px) and
+ blurred with `RenderEffect`.
+- Video backdrop: the server thumbnail, blurred, shown at once. It is then
+ replaced by a frame from the preview mp4 at a random time between 20 %
+ and 80 % of `durationMs` (`MediaMetadataRetriever.getFrameAtTime`, off
+ the main thread, cancelled on post change). On failure the thumbnail
+ stays. First and last frames are avoided because they are often black.
+- Remote:
+ - Up / Down: previous / next post.
+ - Left / Right: seek -10 s / +10 s on video; previous / next slide on a
+ carousel post.
+ - OK: pause / play, applied immediately.
+ - Double OK (second press within about 300 ms): undo that pause toggle
+ and toggle the like instead. A large heart flashes in the center.
+ - Back: exit.
+- Auto-advance: video end, or photo timer (Settings, default 8 s), goes to
+ the next post. Pausing stops the timer.
+- Thin progress bar along the bottom edge for photos (timer) and videos
+ (playback position). Frozen while paused.
+- Small pause icon at the bottom center while paused.
+- Small heart at the bottom right while the post is liked.
+- Media: `previewUrl` (images up to 1500 px wide, video mp4). One ExoPlayer
+ instance, reused across posts.
+
+### Settings
+
+- Change instance (reopens the picker).
+- Viewer password.
+- Photo duration (default 8 s).
+- Background interval (default 15 s).
+
+## Architecture
+
+| Unit | Job |
+| --- | --- |
+| `Api` | `HttpURLConnection` + `org.json`. Folders, folder images, feed, reels, likes, like toggle, health, auth status, login. Resolves relative media URLs against the server URL. Sends `x-foldergram-intent: 1` on mutations. |
+| `Discovery` | Subnet scan for the instance picker. |
+| `Post` | Data class from feed-item JSON: `id`, folder, `mediaType`, width/height, `durationMs`, `thumbnailUrl`, `previewUrl`, `postType`, `mediaItems` (carousel slides). |
+| `Likes` | In-memory set of liked post IDs, loaded from `/api/likes` at start, updated on each toggle. Drives every heart indicator. |
+| `MainActivity` | Top menu and content area; Home, Liked, Folders, Settings as Fragments. |
+| `HomeFragment` | Vertical RecyclerView of horizontal rows, rotating background. |
+| `FolderGridActivity` | One folder, all pages, shuffled grid. |
+| `ViewerActivity` | Plays a list of posts from a start index, or pages an endless source (Reels). Backdrop, progress bar, pause icon, hearts, double-OK. |
+| `Prefs` | `SharedPreferences`: server URL, password, photo duration, background interval. |
+
+- Lists reach the viewer through a static in-memory holder; intents cannot
+ carry hundreds of posts.
+- Session: `CookieHandler.setDefault(new CookieManager())` at app start.
+ `Api`, Glide's default fetcher and ExoPlayer's `DefaultHttpDataSource` all
+ use `HttpURLConnection` and share the cookie. `MediaMetadataRetriever`
+ gets the `Cookie` header passed explicitly.
+- Login runs on app start with the stored password; the cookie lives in
+ memory only.
+- Memory: Glide decodes at view size, backdrops decode small, the viewer
+ uses previews not originals.
+
+## Errors
+
+- Server unreachable or login fails at start: full-screen message with
+ Retry and Change instance. Never a crash.
+- `401` mid-session: one silent re-login and retry; on second failure open
+ Settings.
+- Thumbnail load failure: plain dark tile.
+- Viewer media failure: message over the blurred thumbnail, auto-advance
+ after the photo timer.
+- Like toggle failure: revert the heart, show a toast.
+- Page fetch failure in Reels: retry on the next Down press.
+
+## Testing
+
+- One JVM unit test covering: `Post` parsing (single image, video,
+ carousel), URL resolution, Foldergram health-reply detection, and the
+ double-OK logic as a plain class with an injected clock.
+- On-device check for every UI change: `KEYCODE_WAKEUP`, drive with
+ `adb shell input keyevent`, inspect with `adb exec-out screencap -p`.
+- Before the first build, probe the live instance with `curl` to confirm
+ field names (`postType`, `mediaItems`, `/api/likes` item shape) instead
+ of trusting the upstream docs.
+
+## Out of scope
+
+Search, stories, moments, places, collections, admin features, uploads and
+deletes, phone or tablet layouts, mDNS discovery.