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.
74 lines
2.9 KiB
Markdown
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 |
|