- Web: favicon.ico + png icons under static/, served from the site root
and linked from every page head.
- Android: the artwork becomes the launcher icon — the 432px adaptive
foreground over the gruvbox background, replacing the placeholder vector
drawables.
- Desktop: the icon is the Qt window/taskbar icon (embedded via app.qrc),
and the app is now an installable package — a wrapped binary, a
photog.desktop entry, and hicolor icons — so `nix profile install
./desktop` puts "Photog" in the application menu.
Pick or share several photos at once and post them in one batch, still
against the existing single-image /upload endpoint — no server change.
- Unify the post screen on a list model (PickedItem): a single-photo post
or share is just a batch of one, so there is one code path.
- Multi-select via PickMultipleVisualMedia and multi-share via
ACTION_SEND_MULTIPLE (manifest filter added).
- Per-photo caption and EXIF-prefilled date; each card shows its own
upload status, with a summary and retry-only-failures on re-post.
- Enqueue one WorkManager worker per photo, reusing the durable retry
queue. UploadQueue now appends a per-item suffix to the slug so a burst
enqueued in the same second cannot collide and be dropped by
enqueueUniqueWork.
Native single-photo uploader for POST /upload. Lives in android/ with its
own flake, so the root flake stays Go-only and NixOS consumers of
nixosModules.default do not pull the Android SDK into their lock.
The server is untouched, so the app closes every gap client side:
- The slug is the enqueue-time timestamp, persisted in the work input.
server.go derives the stored slug from the uploaded filename, so reusing
that filename across WorkManager retries overwrites the same post rather
than creating a duplicate. That stands in for an idempotency key.
- EXIF is stripped before upload. The gallery publishes the uploaded file
verbatim as its full-size download, and phone photos carry GPS. JPEG and
PNG drop metadata at marker and chunk level with the compressed pixels
untouched; HEIC is decoded to JPEG because the extension check rejects it.
- Photos flagged for rotation are rotated into the pixels instead of relying
on the orientation tag, which cannot survive the strip and which the
thumbnailer ignores regardless.
- Anything over 18 MiB steps quality, then resolution, to stay inside
nginx's client_max_body_size 20M.
- Credentials are sealed with an AES-GCM key in the Android Keystore. The
key deliberately does not require user authentication, or background
retries could not read it. Basic auth is attached by an interceptor
rather than an Authenticator, which reacts to a 401 by replaying a
multipart body that is not reliably replayable.
- The connection test uses GET /upload. /health is unreachable from outside
because the nginx vhost only proxies location /upload.
WorkManager's SystemForegroundService needs foregroundServiceType declared
on the service entry, not just as a permission, or setForeground throws on
Android 14+. Only lintVitalRelease catches that, never a debug build.
Also anchors the .gitignore patterns. Unanchored, "photogallery" matched the
Kotlin package directory ws/inflo/photogallery/ and silently swallowed every
source file on git add.