diff --git a/CLAUDE.md b/CLAUDE.md index ad011be..2c24119 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,7 @@ Personal static photo gallery with Atom feed and upload API. Single-user. No database, no framework — just a Go binary that watches a directory and regenerates a static site. +An Android app in `android/` posts to the same upload API. ## Architecture @@ -9,7 +10,8 @@ No database, no framework — just a Go binary that watches a directory and rege content/ # images + .toml sidecars (source of truth) public/ # generated output (served by nginx / --serve flag) templates/ # embedded HTML templates -static/ # embedded CSS +static/ # embedded CSS + JS +android/ # Kotlin/Compose uploader app, own flake ``` The binary does three things at once: @@ -17,13 +19,15 @@ The binary does three things at once: - Serves a multipart upload API at `POST /upload` - In dev mode (`--serve`), also serves `public/` as static files -In production nginx sits in front: serves `public/` directly, proxies `/upload` to localhost, htpasswd on the upload endpoint. +Routes: `GET /upload` (HTML upload form), `POST /upload` (multipart), `GET /health`. + +In production nginx sits in front: serves `public/` directly, proxies `/upload` to localhost, htpasswd on the upload endpoint. Note `/health` is **not** proxied — nginx only forwards `location /upload`, so anything outside the box can only reach `/upload`. ## Stack - **Go** — stdlib + `BurntSushi/toml`, `fsnotify/fsnotify`, `golang.org/x/image` - **Templates** — `html/template`, embedded via `embed.FS` -- **Images** — centre-crop to square, Catmull-Rom resize to 600×600 JPEG thumbnails +- **Images** — three renditions per photo: original copy, 600×600 centre-cropped square thumbnail, and a 1600px-longest-side "medium" for the detail page. Catmull-Rom resize. Outputs are skipped when newer than the source. - **Feed** — Atom (`encoding/xml`) - **NixOS** — `flake.nix` + `module.nix`, exports `nixosModules.default` @@ -34,15 +38,36 @@ caption = "Tempelhof at sunrise" date = "2026-06-14" ``` -Missing sidecar → filename as caption, today as date. +Missing sidecar → filename as caption, today as date. Photos sort newest first by that date. ## Design - Gruvbox dark hard colour scheme -- Vollkorn (serif) for headings and captions -- Manjari for Malayalam text (bio line) -- 3-column square grid → 2-column on mobile -- Single photo page with sidebar +- Crimson Pro (serif) for headings and captions — configurable via `-serif-family` / `-fonts-url` +- Manjari for Malayalam text (bio line), via `-ml-family` +- 3-column square grid → 2-column on mobile, 60 photos per page (`/`, `/page/2/`, …) +- Single photo page with sidebar and newer/older navigation + +### Photo page navigation + +`static/photo-nav.js` handles arrow keys and touch swipe. Both read their +destinations from the `[data-nav="newer"]` / `[data-nav="older"]` anchors the +template renders, so a photo with no sibling in that direction has nothing to +find and the gesture rubber-bands. If the script fails to load the links still +work. + +Swipe left → older, right → newer, matching ArrowRight/ArrowLeft. Constraints +worth knowing before touching it: + +- Swipes starting within 24px of a screen edge are ignored — that strip is the browser's own back-gesture. +- Only `pointerType === "touch"`, so desktop drag-to-save is untouched. +- Axis locks once after 10px so vertical drags stay scrolls; `touch-action: pan-y` tells the compositor the same. +- The image is wrapped in an `` to the full-size original, so the click trailing a swipe is suppressed or it opens a new tab. +- `EXIT_MS` in the JS must stay in sync with the transition duration in `style.css`. + +Anything added under `static/` is embedded and copied to `public/static/` +automatically — `assets.go` embeds the whole directory and `copyStatic()` walks +every entry, so new CSS/JS needs no generator change. ## Running locally @@ -54,6 +79,20 @@ go run . --serve # upload: http://localhost:8080/upload ``` +## Android app + +Kotlin + Jetpack Compose uploader: pick or share a photo, add caption and date, post. Credentials (HTTP Basic) are sealed with an Android Keystore AES-GCM key. See `android/README.md` for the detail. + +Its own flake, deliberately separate from the root one so NixOS consumers of `nixosModules.default` don't pull the Android SDK into their lock: + +```bash +cd android +nix develop +./gradlew assembleRelease # app/build/outputs/apk/release/app-release.apk +``` + +The server needs no changes to serve it — every gap (slug collisions, EXIF, HEIC, size caps) is closed client-side. Release builds need `android/keystore.properties`; without it the signing config is skipped and only debug builds work. + ## NixOS module usage ```nix @@ -86,12 +125,19 @@ nix build # succeeds | File | Purpose | |---|---| | `main.go` | Flags, wires generator + watcher + server | -| `generator.go` | Scans content dir, renders HTML + Atom feed, generates thumbs | +| `generator.go` | Scans content dir, renders HTML + Atom feed, generates thumbs/mediums | | `server.go` | Upload API + optional static file serving | | `watcher.go` | fsnotify watcher with 500ms debounce | | `assets.go` | `//go:embed` for templates and static | +| `templates/photo.html` | Single photo page, neighbour links and prefetch | +| `static/photo-nav.js` | Arrow-key and swipe navigation | | `module.nix` | NixOS module (systemd service, tmpfiles, nginx vhost) | | `flake.nix` | `buildGoModule` package + devShell + nixosModules | +| `android/flake.nix` | Android SDK dev shell (separate from the root flake) | + +## Gotchas + +- `.gitignore` patterns here must be anchored. An unanchored `photogallery` also matches the Kotlin package directory `android/app/src/main/java/ws/inflo/photogallery/` and silently swallows every source file in it. ## What's not done yet @@ -99,3 +145,5 @@ nix build # succeeds - Multi-image upload - Delete/edit via UI - Any auth beyond nginx htpasswd +- The Android app has never been run on real hardware +- Photo-page swipe has never been tested on a real touchscreen; the iOS Safari edge-gesture interaction is the untested part