Swipe left for older, right for newer, matching the existing arrow keys.
The image tracks the finger and slides out on commit; the exit animation
covers the page load, so this stays plain static-site navigation.
The keyboard handler moves out of the template into static/photo-nav.js,
which the existing //go:embed of static/ picks up with no generator
changes.
Swipes starting within 24px of a screen edge are left alone — that strip
is the browser's own back-gesture. Only touch pointers are handled, so
desktop drag-to-save still works, and the axis locks after 10px so
vertical drags stay scrolls. With no neighbour that way the image
rubber-bands instead of navigating.
Also prefetch the neighbour medium JPEGs, not just their HTML, or the
next page still blocks on a fresh image request.
The keyboard handler now bails on modifier keys; it previously hijacked
Alt+ArrowLeft, which is browser-back on Linux and Windows.
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.
Move the Newer and Older links to a right-aligned group joined by a
slash so they stop crowding the Back link, and drop the aqua accent so
every link in the row shares the muted serif tone.
Replace the standalone Back link with a three-cell nav row so each
photo page links to its neighbours in the timeline. Prefetch the
neighbouring pages and bind the arrow keys for keyboard navigation.
Emit /page/N/ pages alongside index.html and render a pager at the
bottom of each page. Ships as a plain link that navigates without JS,
and an IntersectionObserver enhancement that fetches the next page and
appends tiles for an infinite-scroll feel.
The profile header, photo page byline and copyright footer previously
hardcoded the author name, handle, bio, and font choices. Move all of
these to CLI flags on the binary and matching options on the NixOS
module so the same build can serve any single-user gallery.
New surface, all with defaults matching the previous hardcoded values:
--author / services.photogallery.author
--handle / services.photogallery.handle
--bio / services.photogallery.bio
--bio-alt / services.photogallery.bioAlt ("" to omit)
--fonts-url / services.photogallery.fontsURL
--serif-family / services.photogallery.serifFamily
--ml-family / services.photogallery.mlFamily
Templates read the two font families from a small inline <style> that
overrides the :root variables, so the stylesheet stays fully static and
the font URL is loaded via the configured stylesheet link.
README updated to describe the new options, the medium/full-size photo
detail flow, and the EXIF-based date prefill on the upload page.
Once a file was picked the preview <img> overlaid the file input and
blocked further clicks, so the drop zone became read-only. Making the
preview and the new 'Click to change' hint pointer-events:none lets
clicks pass through to the input again.
Also parses the JPEG APP1 EXIF segment inline (no dependency) to read
DateTimeOriginal and prefill the date field. Falls back to the file's
lastModified for PNGs or JPEGs without EXIF. The date field stays
editable in either case.
Vollkorn read as too chunky next to the Malayalam bio in Manjari.
Crimson Pro is a calmer book-face serif that pairs more evenly.
Renames the CSS variable from --vk to --serif to reflect that the
value is no longer Vollkorn-specific, and bumps .ml to 17px/1.5
so the Malayalam subtitle sits at a comfortable reading size.
Photo detail page previously showed a square crop of the source image. Now
generate an aspect-preserved medium JPEG (longest side 1600, quality 88)
and display that on the detail page instead. The original file is linked
from a click-through on the image and from a sidebar 'Full size' download
link.
Also skip regeneration of images/thumbs/medium when the destination is
already at least as new as the source, so a rebuild triggered by editing
one sidecar (or restarting the service) no longer redecodes and rescales
every photo.