aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-10-22-mygram-droid-design.md
blob: a2183b056909c7c9e1a945e8cd07252e3c9a079f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# 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`.

## Visual style

- Icons instead of text buttons wherever an icon is clear: top menu,
  Continue tile, Retry / Change instance / Rescan / manual entry, viewer
  overlays (pause, heart).
- Icons are Material Symbols vector drawables (Apache 2.0) copied into
  `res/drawable` as XML, no icon-font or library dependency.
- The focused icon shows its label in small text beside or below it, so
  icon-only controls stay readable from the couch. Unfocused icons show no
  text.
- Every icon has a `contentDescription` (TalkBack).
- Text stays where content is text: instance names, URL and password
  fields, error messages, Settings values.

## 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

Icon bar, always visible except in the viewer. Up from the content
reaches it.

```
[Foldergram]          [Reels] [Liked] [Folders]          [Settings]
 left                          centered                       right
```

- Home is the Foldergram logo, loaded at runtime from the instance's
  `/icon-192.png` (via Glide, cached). It is not bundled: Foldergram is
  AGPLv3 and its assets do not go into this GPLv2-only APK. Fallback: the
  Material home icon.
- Left / Right moves focus across all five in that order.

### 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.