Files
photog/android/README.md
T
puttaalu bb8c7109fa Add tag autocomplete to the Android app
Both tag fields (post and edit) now suggest existing tags as you type,
matching the web upload/manage forms. Suggestions come from a new
GET /api/tags.json that serves the generated public/tags.json. It lives under
/api/ so nginx proxies it to the app, reaching non-browser clients even when a
front-door botwall gates the static site (the same approach as /api/thumb/).

/api/tags.json and /api/thumb/{slug} are both left unguarded: they serve the
same public data as the static site (/tags.json and /thumbs/), so there is
nothing to protect, and keeping them off the auth path means they can never
return 401 — otherwise an nginx auth-fail jail (fail2ban) would ban the app
over an autocomplete poll or a per-row thumbnail load.

A reusable TagField composable filters on the token after the last comma and
completes it on tap; fetchTags() returns an empty list on any failure, so the
field degrades to a plain tags input when the endpoint is absent.
2026-10-20 18:26:18 +02:00

74 lines
2.9 KiB
Markdown

# Gallery Upload — Android client
Native uploader and manager for the photogallery server. Pick or share one or
many photos, add caption/date/tags, and post. A **Manage** screen lists the
published posts and edits their caption/date/tags or deletes them (to the
server's trash) over the same `/api/*` endpoints the web `/manage` uses.
Both tag fields autocomplete from the existing tags (fetched from
`/api/tags.json`, unguarded public data proxied past the botwall), filtering on
the token after the last comma.
Credentials are entered once and sealed with a key held in the Android Keystore.
The server is not modified by this app. Everything it needs, it does client
side; the management calls send no browser headers, so they pass the server's
same-origin (CSRF) check as a non-browser client.
## Build
```bash
cd android
nix develop # Android SDK 36, build-tools 36.0.0, JDK 17, gradle
gradle wrapper # first time only, generates ./gradlew
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
```
The dev shell exports `ANDROID_HOME`, `JAVA_HOME` and a `GRADLE_OPTS` that
points AGP at the nix-provided `aapt2`. Without that last one AGP downloads a
generic-linux `aapt2` from Maven which will not run on NixOS.
## Release signing
`app/build.gradle.kts` wires up a release signing config only when
`android/keystore.properties` exists. Both that file and `*.jks` are
gitignored — keep the keystore outside the repo and back it up somewhere you
will still have in five years, because losing it means you can never upgrade an
installed build in place.
```bash
mkdir -p ~/keys
keytool -genkeypair -v \
-keystore ~/keys/photogallery-release.jks \
-alias photogallery \
-keyalg RSA -keysize 4096 -validity 10000
```
Then write `android/keystore.properties`:
```properties
storeFile=/home/you/keys/photogallery-release.jks
storePassword=…
keyAlias=photogallery
keyPassword=…
```
```bash
./gradlew assembleRelease
```
Without `keystore.properties` the release build still runs, it just comes out
unsigned.
## How it maps onto the server
| Server behaviour | What the app does about it |
|---|---|
| Slug derived from the uploaded filename | Sends `yyyy-MM-dd-HHmmss.jpg`, generated once at enqueue |
| Same filename overwrites the same post | Reused across retries, which makes retries idempotent |
| Uploaded file published verbatim as "full size" | Strips EXIF, including GPS, before upload |
| Extension check rejects HEIC | Converts HEIC to JPEG; JPEG and PNG pass through losslessly |
| nginx `client_max_body_size 20M` | Steps quality, then resolution, if the result exceeds 18 MiB |
| Thumbnailer ignores EXIF orientation | Bakes rotation into the pixels rather than relying on the tag |
| nginx only proxies `/upload`, so `/health` 404s | Tests credentials with `GET /upload` instead |
| Replies `uploaded <slug>.jpg` in plain text | Parses that into `<base>/photo/<slug>/` for the "Open" link |