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

4 KiB
Raw Permalink Blame History

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):

{
  "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.