Update CLAUDE.md for pagination, medium images, swipe nav and the Android app
Also corrects the serif font, which is Crimson Pro rather than Vollkorn, and notes that .gitignore patterns here have to be anchored or they eat the Kotlin package directory.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
Personal static photo gallery with Atom feed and upload API. Single-user.
|
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.
|
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
|
## 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)
|
content/ # images + .toml sidecars (source of truth)
|
||||||
public/ # generated output (served by nginx / --serve flag)
|
public/ # generated output (served by nginx / --serve flag)
|
||||||
templates/ # embedded HTML templates
|
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:
|
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`
|
- Serves a multipart upload API at `POST /upload`
|
||||||
- In dev mode (`--serve`), also serves `public/` as static files
|
- 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
|
## Stack
|
||||||
|
|
||||||
- **Go** — stdlib + `BurntSushi/toml`, `fsnotify/fsnotify`, `golang.org/x/image`
|
- **Go** — stdlib + `BurntSushi/toml`, `fsnotify/fsnotify`, `golang.org/x/image`
|
||||||
- **Templates** — `html/template`, embedded via `embed.FS`
|
- **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`)
|
- **Feed** — Atom (`encoding/xml`)
|
||||||
- **NixOS** — `flake.nix` + `module.nix`, exports `nixosModules.default`
|
- **NixOS** — `flake.nix` + `module.nix`, exports `nixosModules.default`
|
||||||
|
|
||||||
@@ -34,15 +38,36 @@ caption = "Tempelhof at sunrise"
|
|||||||
date = "2026-06-14"
|
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
|
## Design
|
||||||
|
|
||||||
- Gruvbox dark hard colour scheme
|
- Gruvbox dark hard colour scheme
|
||||||
- Vollkorn (serif) for headings and captions
|
- Crimson Pro (serif) for headings and captions — configurable via `-serif-family` / `-fonts-url`
|
||||||
- Manjari for Malayalam text (bio line)
|
- Manjari for Malayalam text (bio line), via `-ml-family`
|
||||||
- 3-column square grid → 2-column on mobile
|
- 3-column square grid → 2-column on mobile, 60 photos per page (`/`, `/page/2/`, …)
|
||||||
- Single photo page with sidebar
|
- 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 `<a>` 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
|
## Running locally
|
||||||
|
|
||||||
@@ -54,6 +79,20 @@ go run . --serve
|
|||||||
# upload: http://localhost:8080/upload
|
# 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
|
## NixOS module usage
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
@@ -86,12 +125,19 @@ nix build # succeeds
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `main.go` | Flags, wires generator + watcher + server |
|
| `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 |
|
| `server.go` | Upload API + optional static file serving |
|
||||||
| `watcher.go` | fsnotify watcher with 500ms debounce |
|
| `watcher.go` | fsnotify watcher with 500ms debounce |
|
||||||
| `assets.go` | `//go:embed` for templates and static |
|
| `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) |
|
| `module.nix` | NixOS module (systemd service, tmpfiles, nginx vhost) |
|
||||||
| `flake.nix` | `buildGoModule` package + devShell + nixosModules |
|
| `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
|
## What's not done yet
|
||||||
|
|
||||||
@@ -99,3 +145,5 @@ nix build # succeeds
|
|||||||
- Multi-image upload
|
- Multi-image upload
|
||||||
- Delete/edit via UI
|
- Delete/edit via UI
|
||||||
- Any auth beyond nginx htpasswd
|
- 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
|
||||||
|
|||||||
Reference in New Issue
Block a user