ultimate-media-tracker/android/README.md
Tronax de353f0218
feat: add Android app and end-to-end encrypted library sync
Introduce the Kotlin/Compose Android companion app and extend the TMDB
proxy into a combo server that synchronizes the whole library as an
encrypted snapshot, with all three components (Go server, Android,
desktop) sharing one zero-knowledge crypto envelope and JSON schema.

- server: add PostgreSQL-backed /sync/library (GET/PUT) with optimistic
  concurrency (revision + 409 on conflict); sync stays disabled unless
  DATABASE_URL is set. Add docker-compose with Postgres.
- desktop: add libsodium CryptoEnvelope, LibrarySerializer and SyncClient;
  storage-mode and passphrase settings; a "Sync now" toolbar action with
  conflict resolution.
- android: Room-backed library with local/cloud storage modes, encrypted
  sync client, and settings UI. The proxy server (URL + token) can be
  configured as a TMDB proxy even in local-only mode.

Crypto is identical across platforms: Argon2id (libsodium crypto_pwhash,
INTERACTIVE) + XChaCha20-Poly1305 in a versioned "UMTS" envelope, so a
snapshot encrypted on one client decrypts on the other.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-21 00:35:54 +02:00

100 lines
4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Ultimate Media Tracker Android
Mobile Companion-App zur Desktop-Anwendung, gebaut mit **Kotlin**, **Jetpack
Compose** und **Material 3**. Die App zeigt deine Bibliothek an, die du in der
Desktop-App als JSON exportierst und hier importierst.
## Status (MVP-Gerüst)
Enthalten ist ein lauffähiges Grundgerüst mit:
- Datenmodell, das 1:1 zur Desktop-App passt (`MediaType`, `WatchStatus`,
Segmente/Einheiten, Fortschritt).
- Lokale Speicherung mit **Room**.
- **JSON-Import** über den System-Dateidialog (`kotlinx.serialization`).
- **Navigation** (Bibliotheksliste → Detailansicht) mit Navigation-Compose.
- Material-3-Theme (dunkel/hell + Material-You-Dynamic-Color ab Android 12).
- Bibliotheks-Übersicht als Karten-Grid mit Such- und Typ-Filter.
- Detailansicht mit Cover, Status, Bewertung, Fortschrittsbalken und
**standardmäßig zugeklappten** Staffeln/Bänden (wie in der Desktop-App).
Eine mitgelieferte Beispielbibliothek (`app/src/main/assets/sample-library.json`)
wird beim ersten Start geladen, damit die App nicht leer ist.
## Sync (lokal oder verschlüsselte Cloud)
Unter **Einstellungen** (Zahnrad oben rechts) wählst du den Speicherort:
- **Nur lokal (SQLite):** Die Bibliothek bleibt ausschließlich auf dem Gerät.
- **Cloud-Sync:** Die komplette Bibliothek wird als ein Snapshot
**Ende-zu-Ende verschlüsselt** (Argon2id + XChaCha20-Poly1305 via libsodium)
und über den Kombi-Server (`/sync/library`) abgeglichen. Der Server speichert
nur Chiffretext Passphrase und Klartext verlassen das Gerät nie.
Für den Cloud-Sync trägst du **Server-URL** und **Zugriffstoken** (beides aus der
Proxy-Anmeldung, identisch zur Desktop-App) sowie eine frei gewählte
**Sync-Passphrase** ein. Dieselbe Passphrase muss auf allen Geräten gesetzt sein.
Server-URL, Token und Passphrase liegen in `EncryptedSharedPreferences`
(Android-Keystore).
Synchronisiert wird die **ganze Bibliothek als Snapshot** mit Revisionsnummer
(Last-Write-Wins). Beim Hochladen prüft der Server die Revision; bei einem
Konflikt (zwischenzeitliche Änderung) bietet die App an, den Serverstand zu laden
oder lokal zu überschreiben. Das Wolken-Symbol oben rechts lädt direkt herunter.
## Öffnen & Bauen
1. Den Ordner `android/` in **Android Studio** (Ladybug oder neuer) öffnen.
2. Android Studio erzeugt beim ersten Sync den Gradle-Wrapper und lädt die
Abhängigkeiten. Alternativ per CLI: `gradle wrapper` im `android/`-Ordner,
danach `./gradlew assembleDebug`.
3. Auf einem Gerät/Emulator mit **mindestens Android 8.0 (API 26)** ausführen.
## JSON-Exportformat
Die App liest eine Datei dieser Form (entspricht dem Desktop-`MediaItem`):
```json
{
"version": 1,
"exportedAt": "2026-06-20T12:00:00Z",
"items": [
{
"type": "Series", // Movie | Series | Manga | Book | Game
"title": "Solo Leveling",
"originalTitle": "...",
"localizedTitles": { "de": "...", "en": "..." },
"coverUrl": "https://.../cover.jpg",
"genres": ["Action"],
"tags": [],
"franchise": "",
"status": "InProgress", // PlanToWatch | InProgress | Completed | OnHold | Dropped
"rating": 9, // 0..10
"favorite": true,
"year": 2024,
"overview": "...",
"notes": "",
"segments": [
{ "number": 1, "title": "Staffel 1", "units": [
{ "number": 1, "title": "Folge 1", "watched": true, "watchedDate": "" }
]}
],
"customFields": {},
"dateAdded": "",
"dateCompleted": "",
"externalId": "127532",
"externalSource": "tmdb"
}
]
}
```
Wichtig: Der Import **ersetzt** die gesamte lokale Bibliothek (alles-oder-nichts;
bei einem Parse-Fehler bleiben die vorhandenen Daten erhalten). `coverPath` aus
der Desktop-DB wird ignoriert auf dem Handy zählt `coverUrl`.
## Noch offen / nächste Schritte
- Bearbeiten/Fortschritt auf dem Handy abhaken (aktuell read-only); Änderungen
werden danach hochgeladen.
- Online-Suche über den TMDB-Proxy / AniList / RAWG / OpenLibrary.