diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt
index 1c54fd0d3..221e77620 100644
--- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt
+++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt
@@ -48,6 +48,7 @@ import com.nuvio.app.features.trakt.TraktLibraryStorage
import com.nuvio.app.features.trakt.TraktSettingsStorage
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
import com.nuvio.app.features.updater.AndroidAppUpdaterPlatform
+import com.nuvio.app.core.ui.CardDepthStyleStorage
import com.nuvio.app.core.ui.PosterCardStyleStorage
import com.nuvio.app.features.watched.WatchedStorage
import com.nuvio.app.features.streams.StreamLinkCacheStorage
@@ -90,6 +91,7 @@ class MainActivity : AppCompatActivity() {
SearchHistoryStorage.initialize(applicationContext)
SeasonViewModeStorage.initialize(applicationContext)
PosterCardStyleStorage.initialize(applicationContext)
+ CardDepthStyleStorage.initialize(applicationContext)
DebridSettingsStorage.initialize(applicationContext)
TmdbSettingsStorage.initialize(applicationContext)
MdbListSettingsStorage.initialize(applicationContext)
diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.android.kt
new file mode 100644
index 000000000..a0eaa831e
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.android.kt
@@ -0,0 +1,26 @@
+package com.nuvio.app.core.ui
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.nuvio.app.core.storage.ProfileScopedKey
+
+internal actual object CardDepthStyleStorage {
+ private const val preferencesName = "nuvio_card_depth_style"
+ private const val payloadKey = "card_depth_style_payload"
+
+ private var preferences: SharedPreferences? = null
+
+ fun initialize(context: Context) {
+ preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
+ }
+
+ actual fun loadPayload(): String? =
+ preferences?.getString(ProfileScopedKey.of(payloadKey), null)
+
+ actual fun savePayload(payload: String) {
+ preferences
+ ?.edit()
+ ?.putString(ProfileScopedKey.of(payloadKey), payload)
+ ?.apply()
+ }
+}
diff --git a/composeApp/src/commonMain/composeResources/values-bg/strings.xml b/composeApp/src/commonMain/composeResources/values-bg/strings.xml
index d02723c2f..b14cd5fff 100644
--- a/composeApp/src/commonMain/composeResources/values-bg/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-bg/strings.xml
@@ -433,7 +433,9 @@
Изтегли последна версия
Провери за актуализации
Управлявай добавки и източници за discovery.
+ Управлявайте добавките и изберете какво да се показва в Nuvio.
Управлявай изтеглените филми и епизоди.
+ Управлявайте запазеното на това устройство.
Изтегляния
ОБЩИ
Управлявай наличните интеграции
@@ -1845,11 +1847,13 @@
Грешка в торента: %1$s
Превключи
- Свържете собствен медиен сървър, за да разглеждате и възпроизвеждате от личната си библиотека.
- Добавете URL на сървър по-горе, когато искате Nuvio да показва вашата лична библиотека.
- Няма свързани лични библиотеки.
- URL на сървър
- Добави
+ Пренесете библиотеката си в Nuvio с добавките, които използвате.
+ Добавете добавка, за да започнете да изграждате библиотеката си.
+ Настройте Nuvio по свой начин.
+ URL на добавка
+ Добавяне на добавка
+ Все още няма нищо тук
+ Текущите ви добавки все още нямат какво да покажат тук.
Сървър и поддръжка за този месец
Покрито. Допълнителната подкрепа вече отива за разработка.
След 100% допълнителната подкрепа отива за разработка.
@@ -1861,7 +1865,7 @@
Още действия
Неизвестна грешка в торента
Език на устройството
- Свържете лични медийни източници и управлявайте достъпа до собствената си библиотека.
+ Добавяйте и управлявайте добавките, които използвате с Nuvio.
Изпращане на времеви маркери за интро и аутро
Предайте откритите времеви маркери за интро и аутро към външния плейър за автоматично прескачане. Работи само в плейъри, които го поддържат; останалите плейъри го игнорират.
Жестове с докосване
diff --git a/composeApp/src/commonMain/composeResources/values-cs/strings.xml b/composeApp/src/commonMain/composeResources/values-cs/strings.xml
index 405334190..039e9d5d8 100644
--- a/composeApp/src/commonMain/composeResources/values-cs/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-cs/strings.xml
@@ -32,11 +32,13 @@
Nedostupné
Konfigurovat doplněk
Smazat doplněk
- Připojte svůj vlastní mediální server a procházejte i přehrávejte ze své osobní knihovny.
- Přidejte výše adresu URL serveru, pokud chcete, aby Nuvio zobrazovalo vaši soukromou knihovnu.
- Nejsou připojeny žádné osobní knihovny.
- URL serveru
- Přidat
+ Přeneste svou knihovnu do Nuvia pomocí doplňků, které používáte.
+ Přidejte doplněk a začněte vytvářet svou knihovnu.
+ Přizpůsobte si Nuvio.
+ URL doplňku
+ Přidat doplněk
+ Zatím tu nic není
+ Vaše současné doplňky tu zatím nemají co zobrazit.
Přidejte URL manifestu, abyste mohli do Nuvio začít načítat katalogy, metadata, streamy nebo titulky.
Zatím nejsou nainstalovány žádné doplňky.
Zadejte URL doplňku.
@@ -442,7 +444,9 @@
Stáhnout nejnovější verzi
Zkontrolovat aktualizace
Správa doplňků a zdrojů objevování.
+ Spravujte doplňky a vyberte, co se má v Nuviu zobrazovat.
Spravujte své stažené filmy a epizody.
+ Spravujte obsah uložený v tomto zařízení.
Stažené položky
OBECNÉ
Spravovat dostupné integrace
@@ -638,7 +642,7 @@
DOMŮ
ZDROJE
Instalovat, odstraňovat, obnovovat a třídit vaše zdroje obsahu.
- Připojit osobní mediální zdroje a spravovat přístup k vlastní knihovně.
+ Přidávejte a spravujte doplňky, které používáte s Nuviem.
Instalovat repozitáře pro JavaScriptové scarpování a testovat poskytovatele interně.
Upravit rozvržení domovské obrazovky, viditelnost obsahu a chování plakátů.
Nastavení pro obrazovku detailu a epizody.
diff --git a/composeApp/src/commonMain/composeResources/values-de/strings.xml b/composeApp/src/commonMain/composeResources/values-de/strings.xml
index 3afb1161c..09a975020 100644
--- a/composeApp/src/commonMain/composeResources/values-de/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-de/strings.xml
@@ -27,6 +27,13 @@
Nicht verfügbar
Addon konfigurieren
Addon löschen
+ Bring deine Mediathek mit den Addons, die du nutzt, zu Nuvio.
+ Füge ein Addon hinzu, um deine Mediathek aufzubauen.
+ Richte Nuvio für dich ein.
+ Addon-URL
+ Addon hinzufügen
+ Hier gibt es noch nichts
+ Deine aktuellen Addons zeigen hier noch nichts an.
Füge eine Manifest-URL hinzu, um Kataloge, Metadaten, Streams oder Untertitel in Nuvio zu laden.
Noch keine Addons installiert.
Gib eine Addon-URL ein.
@@ -383,7 +390,9 @@
Suche nach neuen Versionen der App.
Nach Updates suchen
Verwalte Addons und Entdeckungsquellen.
+ Verwalte Addons und wähle aus, was in Nuvio angezeigt wird.
Verwalte deine heruntergeladenen Filme und Episoden.
+ Verwalte, was du auf diesem Gerät gespeichert hast.
Downloads
ALLGEMEIN
TMDB- und MDBList-Dienste verbinden.
@@ -519,6 +528,7 @@
START
QUELLEN
Installiere, entferne, aktualisiere und sortiere deine Inhaltsquellen.
+ Füge die Addons hinzu, die du mit Nuvio nutzt, und verwalte sie.
Installiere JavaScript-Scraper-Repositories und teste Anbieter intern.
Lege fest, welche Kataloge auf der Startseite und in welcher Reihenfolge erscheinen.
Detail-Abschnitte deaktivieren und alles unterhalb des Hero neu anordnen.
diff --git a/composeApp/src/commonMain/composeResources/values-el/strings.xml b/composeApp/src/commonMain/composeResources/values-el/strings.xml
index 4c2fd1766..01c48e62d 100644
--- a/composeApp/src/commonMain/composeResources/values-el/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-el/strings.xml
@@ -233,7 +233,9 @@
Ελέγξτε για νέες εκδόσεις της εφαρμογής.
Έλεγχος για ενημερώσεις
Διαχείριση πρόσθετων και πηγών ανακάλυψης.
+ Διαχειριστείτε τα πρόσθετα και επιλέξτε τι εμφανίζεται στο Nuvio.
Διαχειριστείτε τις κατεβασμένες ταινίες και επεισόδιά σας.
+ Διαχειριστείτε όσα έχετε αποθηκεύσει σε αυτή τη συσκευή.
Λήψεις
ΓΕΝΙΚΑ
Σύνδεση υπηρεσιών TMDB και MDBList.
@@ -1061,11 +1063,13 @@
Αποθήκευση…
Εναλλαγή
Επικύρωση
- Συνδέστε τον δικό σας διακομιστή μέσων για να περιηγηθείτε και να αναπαραγάγετε από τη δική σας προσωπική βιβλιοθήκη.
- Προσθέστε μια διεύθυνση URL διακομιστή παραπάνω όταν θέλετε το Nuvio να εμφανίζει τη δική σας ιδιωτική βιβλιοθήκη.
- Δεν υπάρχουν προσωπικές βιβλιοθήκες συνδεδεμένες.
- Διεύθυνση URL διακομιστή
- Προσθήκη
+ Φέρτε τη βιβλιοθήκη σας στο Nuvio με τα πρόσθετα που χρησιμοποιείτε.
+ Προσθέστε ένα πρόσθετο για να αρχίσετε να δημιουργείτε τη βιβλιοθήκη σας.
+ Κάντε το Nuvio δικό σας.
+ URL πρόσθετου
+ Προσθήκη πρόσθετου
+ Δεν υπάρχει τίποτα εδώ ακόμη
+ Τα πρόσθετα που χρησιμοποιείτε δεν έχουν ακόμη τίποτα να εμφανίσουν εδώ.
Απενεργοποιημένο
Το manifest είναι κενό "%1$s"
Σύνδεση λογαριασμού
@@ -1520,7 +1524,7 @@
Device Γλώσσα
Liquid Glass
Use the native iPhone tab bar on iOS 26 and later.
- Connect personal media sources and manage access to your own Βιβλιοθήκη.
+ Προσθέστε και διαχειριστείτε τα πρόσθετα που χρησιμοποιείτε με το Nuvio.
Blur next episode thumbnails in Συνέχεια Watching to avoid spoilers.
Blur Unwatched in Συνέχεια Watching
Ταξινόμηση ORDER
diff --git a/composeApp/src/commonMain/composeResources/values-es/strings.xml b/composeApp/src/commonMain/composeResources/values-es/strings.xml
index e591930a4..02427ae4a 100644
--- a/composeApp/src/commonMain/composeResources/values-es/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-es/strings.xml
@@ -32,11 +32,13 @@
No disponible
Configurar complemento
Eliminar complemento
- Conecte su propio servidor multimedia para navegar y reproducir desde su biblioteca personal.
- Agregue una URL de servidor arriba cuando desee que Nuvio muestre su biblioteca privada.
- No hay bibliotecas personales conectadas.
- URL del servidor
- Agregar
+ Lleva tu biblioteca a Nuvio con los complementos que utilizas.
+ Agrega un complemento para empezar a crear tu biblioteca.
+ Haz tuyo Nuvio.
+ URL del complemento
+ Agregar complemento
+ Aún no hay nada aquí
+ Tus complementos actuales aún no tienen nada que mostrar aquí.
Agrega una URL de manifiesto para empezar a cargar catálogos, metadatos, streams o subtítulos en Nuvio.
Aún no hay complementos instalados.
Introduce una URL de complemento.
@@ -442,7 +444,9 @@
Buscar nuevas versiones de la app.
Buscar actualizaciones
Administra complementos y fuentes de descubrimiento.
+ Administra tus complementos y elige qué aparece en Nuvio.
Administra tus películas y episodios descargados.
+ Administra lo que has guardado en este dispositivo.
Descargas
GENERAL
Conecta los servicios TMDB y MDBList.
@@ -638,7 +642,7 @@
INICIO
FUENTES
Instala, elimina, actualiza y ordena tus fuentes de contenido.
- Conecte fuentes de medios personales y administre el acceso a su propia biblioteca.
+ Agrega y administra los complementos que utilizas con Nuvio.
Instala repositorios de scrapers en JavaScript y prueba proveedores internamente.
Controla qué catálogos aparecen en Inicio y en qué orden.
Desactiva secciones de detalles y reordena todo debajo del Destacado.
diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml
index ffb979e29..a6a536650 100644
--- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml
@@ -33,11 +33,13 @@
Indisponible
Configurer l’addon
Supprimer l’addon
- Connectez votre propre serveur multimédia pour parcourir et lire depuis votre bibliothèque personnelle.
- Ajoutez une URL de serveur ci-dessus pour que Nuvio affiche votre bibliothèque privée.
- Aucune bibliothèque personnelle connectée.
- URL du serveur
- Ajouter
+ Retrouvez votre bibliothèque dans Nuvio avec les addons que vous utilisez.
+ Ajoutez un addon pour commencer à créer votre bibliothèque.
+ Personnalisez Nuvio.
+ URL de l’addon
+ Ajouter un addon
+ Rien à afficher pour le moment
+ Vos addons actuels n’ont encore rien à afficher ici.
Ajoutez une URL de manifeste pour commencer à charger des catalogues, métadonnées, streams ou sous-titres dans Nuvio.
Aucun addon installé.
Saisissez une URL d’addon.
@@ -434,7 +436,9 @@
Rechercher de nouvelles versions de l’application.
Vérifier les mises à jour
Gérez les addons et sources de découverte.
+ Gérez vos addons et choisissez ce qui apparaît dans Nuvio.
Gérez vos films et épisodes téléchargés.
+ Gérez ce que vous avez enregistré sur cet appareil.
Téléchargements
GÉNÉRAL
Connectez les services TMDB et MDBList.
@@ -648,7 +652,7 @@
ACCUEIL
SOURCES
Installez, supprimez, mettez à jour et ordonnez vos sources de contenu.
- Connectez des sources multimédias personnelles et gérez l’accès à votre propre bibliothèque.
+ Ajoutez et gérez les addons que vous utilisez avec Nuvio.
Installez des dépôts de scrapers JavaScript et testez des fournisseurs en interne.
Contrôle quels catalogues apparaissent à l’accueil et dans quel ordre.
Désactivez des sections de détails et réorganisez tout sous le Hero.
diff --git a/composeApp/src/commonMain/composeResources/values-hu/strings.xml b/composeApp/src/commonMain/composeResources/values-hu/strings.xml
index 6979d2fa7..3dda499ab 100644
--- a/composeApp/src/commonMain/composeResources/values-hu/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-hu/strings.xml
@@ -401,7 +401,9 @@
A legújabb verzió letöltése
Frissítések keresése
Kiegészítők és tartalomforrások kezelése
+ Kezeld a kiegészítőket, és válaszd ki, mi jelenjen meg a Nuvióban.
A letöltött filmek és epizódok kezelése
+ Kezeld az eszközre mentett tartalmakat.
Letöltések
ÁLTALÁNOS
Elérhető integrációk kezelése
@@ -1339,11 +1341,13 @@
GB
Váltás
Kikapcsolva
- Csatlakoztasd saját médiaszerveredet a személyes könyvtárad böngészéséhez és lejátszásához.
- Adj meg egy szerver URL-t fent, ha szeretnéd, hogy a Nuvio megjelenítse a privát könyvtáradat.
- Nincsenek csatlakoztatott személyes könyvtárak.
- Szerver URL
- Hozzáadás
+ Hozd el a könyvtáradat a Nuvióba a használt kiegészítőkkel.
+ Adj hozzá egy kiegészítőt a könyvtárad felépítéséhez.
+ Szabd személyre a Nuviót.
+ Kiegészítő URL-je
+ Kiegészítő hozzáadása
+ Itt még nincs semmi
+ A jelenlegi kiegészítőid még nem jelenítenek meg itt semmit.
28,12
18,35
2020-01-01
@@ -1397,7 +1401,7 @@
Érték megjelenítése
Kártya
TV-stílusú fekvő kártya
- Személyes médiaforrások csatlakoztatása és a saját könyvtáradhoz való hozzáférés kezelése.
+ Add hozzá és kezeld a Nuvióval használt kiegészítőket.
Felhő könyvtár
A csatlakoztatott fiókjaidban már meglévő fájlok böngészése és lejátszása.
Feloldás ezzel
diff --git a/composeApp/src/commonMain/composeResources/values-id/strings.xml b/composeApp/src/commonMain/composeResources/values-id/strings.xml
index 35a923d9f..0c79d268d 100644
--- a/composeApp/src/commonMain/composeResources/values-id/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-id/strings.xml
@@ -31,6 +31,13 @@
Tidak Tersedia
Konfigurasi addon
Hapus addon
+ Bawa pustaka Anda ke Nuvio dengan addon yang Anda gunakan.
+ Tambahkan addon untuk mulai membangun pustaka Anda.
+ Jadikan Nuvio milik Anda.
+ URL Addon
+ Tambah Addon
+ Belum ada apa pun di sini
+ Addon yang Anda gunakan belum memiliki apa pun untuk ditampilkan di sini.
Tambahkan URL manifes untuk mulai memuat katalog, metadata, streaming, atau subtitle ke Nuvio.
Belum ada addon yang terpasang.
Masukkan URL addon.
@@ -412,7 +419,9 @@
Unduh versi terbaru
Periksa pembaruan
Kelola addon dan sumber penemuan.
+ Kelola addon dan pilih apa yang tampil di Nuvio.
Kelola film dan episode yang telah diunduh.
+ Kelola yang Anda simpan di perangkat ini.
Unduhan
UMUM
Kelola integrasi yang tersedia
@@ -597,6 +606,7 @@
BERANDA
SUMBER
Pasang, hapus, perbarui, dan urutkan sumber konten Anda.
+ Tambahkan dan kelola addon yang Anda gunakan dengan Nuvio.
Pasang repositori scraper JavaScript dan uji penyedia secara internal.
Sesuaikan tata letak beranda, visibilitas konten, dan poster
Pengaturan untuk layar detail dan episode.
diff --git a/composeApp/src/commonMain/composeResources/values-in/strings.xml b/composeApp/src/commonMain/composeResources/values-in/strings.xml
index e442e971d..b56c75ce8 100644
--- a/composeApp/src/commonMain/composeResources/values-in/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-in/strings.xml
@@ -31,6 +31,13 @@
Tidak Tersedia
Konfigurasi addon
Hapus addon
+ Bawa pustaka Anda ke Nuvio dengan addon yang Anda gunakan.
+ Tambahkan addon untuk mulai membangun pustaka Anda.
+ Jadikan Nuvio milik Anda.
+ URL Addon
+ Tambah Addon
+ Belum ada apa pun di sini
+ Addon yang Anda gunakan belum memiliki apa pun untuk ditampilkan di sini.
Tambahkan URL manifes untuk mulai memuat katalog, metadata, streaming, atau subtitle ke Nuvio.
Belum ada addon yang terpasang.
Masukkan URL addon.
@@ -412,7 +419,9 @@
Unduh versi terbaru
Periksa pembaruan
Kelola addon dan sumber penemuan.
+ Kelola addon dan pilih apa yang tampil di Nuvio.
Kelola film dan episode yang telah diunduh.
+ Kelola yang Anda simpan di perangkat ini.
Unduhan
UMUM
Kelola integrasi yang tersedia
@@ -597,6 +606,7 @@
BERANDA
SUMBER
Pasang, hapus, perbarui, dan urutkan sumber konten Anda.
+ Tambahkan dan kelola addon yang Anda gunakan dengan Nuvio.
Pasang repositori scraper JavaScript dan uji penyedia secara internal.
Sesuaikan tata letak beranda, visibilitas konten, dan poster
Pengaturan untuk layar detail dan episode.
diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml
index 035580c22..a4cfd0c42 100644
--- a/composeApp/src/commonMain/composeResources/values-it/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml
@@ -27,6 +27,13 @@
Non Disponibile
Configura addon
Cancella addon
+ Porta la tua libreria su Nuvio con gli addon che usi.
+ Aggiungi un addon per iniziare a creare la tua libreria.
+ Fai tuo Nuvio.
+ URL addon
+ Aggiungi addon
+ Non c’è ancora niente qui
+ Gli addon che usi non hanno ancora nulla da mostrare qui.
Aggiungi un manifest URL per iniziare a caricare cataloghi , metadata, flussi o sottotitoli dentro Nuvio.
Nessun addon installato ancora.
Inserisci l'URL dell'addon.
@@ -245,7 +252,9 @@
Controlla se ci sono nuove versioni dell'app.
Verifica aggiornamenti
Gestisci gli addon e le sorgenti di scoperta.
+ Gestisci gli addon e scegli cosa mostrare in Nuvio.
Gestisci i film e gli episodi scaricati.
+ Gestisci ciò che hai salvato su questo dispositivo.
Download
GENERALI
Collega i servizi TMDB e MDBList.
@@ -382,6 +391,7 @@
HOME
SORGENTI
Installa, rimuovi, aggiorna e ordina le tue sorgenti di contenuto.
+ Aggiungi e gestisci gli addon che usi con Nuvio.
Installa repository di scraper JavaScript e testa i provider internamente.
Controlla quali cataloghi appaiono in Home e in quale ordine.
Disabilita le sezioni dei dettagli e riordina tutto ciò che sta sotto l'elemento Hero.
diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings.xml b/composeApp/src/commonMain/composeResources/values-ja/strings.xml
index 45688cd97..885e1c81c 100644
--- a/composeApp/src/commonMain/composeResources/values-ja/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ja/strings.xml
@@ -32,11 +32,13 @@
利用不可
アドオンを設定
アドオンを削除
- 独自のメディアサーバーを接続して、個人ライブラリを参照・再生します。
- Nuvioに個人ライブラリを表示させたい場合は、上にサーバーURLを追加してください。
- 接続された個人ライブラリはありません。
- サーバーURL
- 追加
+ いつものアドオンで、ライブラリをNuvioにまとめましょう。
+ アドオンを追加して、ライブラリを作り始めましょう。
+ Nuvioを自分好みに。
+ アドオンURL
+ アドオンを追加
+ まだ何もありません
+ 現在のアドオンには、ここに表示できるものがまだありません。
マニフェストURLを追加して、カタログ・メタデータ・ストリーム・字幕をNuvioに読み込みましょう。
アドオンがありません
アドオンのURLを入力してください。
@@ -442,7 +444,9 @@
最新リリースをダウンロード
アップデートを確認
アドオンと探索ソースを管理
+ アドオンを管理して、Nuvioに表示する内容を選べます。
ダウンロードした映画とエピソードを管理
+ このデバイスに保存した項目を管理します。
ダウンロード
一般
利用可能な連携を管理
@@ -638,7 +642,7 @@
ホーム
ソース
コンテンツソースのインストール・削除・更新・並べ替え
- 個人メディアソースを接続し、自分専用ライブラリへのアクセスを管理します。
+ Nuvioで使うアドオンを追加・管理します。
JavaScriptスクレイパーリポジトリのインストールと内部テスト
ホームレイアウト・コンテンツ表示・ポスターの動作を調整
詳細画面とエピソード画面の設定
diff --git a/composeApp/src/commonMain/composeResources/values-nb/strings.xml b/composeApp/src/commonMain/composeResources/values-nb/strings.xml
index ad7cea8c3..9957c3dbc 100644
--- a/composeApp/src/commonMain/composeResources/values-nb/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-nb/strings.xml
@@ -29,6 +29,13 @@
Utilgjengelig
Konfigurer tillegg
Slett tillegg
+ Ta med biblioteket ditt til Nuvio med tilleggene du bruker.
+ Legg til et tillegg for å begynne å bygge biblioteket ditt.
+ Gjør Nuvio til ditt.
+ URL til tillegg
+ Legg til tillegg
+ Det er ingenting her ennå
+ Tilleggene du bruker, har ingenting å vise her ennå.
Legg til en manifest-URL for å starte lasting av kataloger, metadata, strømmer eller undertekster i Nuvio.
Ingen tillegg installert ennå.
Skriv inn en tilleggs-URL.
@@ -400,7 +407,9 @@
Last ned nyeste versjon
Se etter oppdateringer
Administrer tillegg og oppdagelseskilder.
+ Administrer tillegg og velg hva som vises i Nuvio.
Administrer nedlastede filmer og episoder.
+ Administrer det du har lagret på denne enheten.
Nedlastinger
GENERELT
Administrer tilgjengelige integrasjoner
@@ -581,6 +590,7 @@
HJEM
KILDER
Installer, fjern, oppdater og sorter innholdskilder.
+ Legg til og administrer tilleggene du bruker med Nuvio.
Installer JavaScript-scraper-repositories og test providere internt.
Juster hjemmeoppsett, synlighet og plakat-oppførsel.
Innstillinger for detalj- og episodeskjermer.
diff --git a/composeApp/src/commonMain/composeResources/values-nl/strings.xml b/composeApp/src/commonMain/composeResources/values-nl/strings.xml
index 59504af97..c2fdc267f 100644
--- a/composeApp/src/commonMain/composeResources/values-nl/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-nl/strings.xml
@@ -32,11 +32,13 @@
Niet beschikbaar
Addon configureren
Addon verwijderen
- Verbind je eigen mediaserver om je persoonlijke bibliotheek te bekijken en af te spelen.
- Voeg hierboven een server-URL toe als je wilt dat Nuvio je privébibliotheek toont.
- Geen persoonlijke bibliotheken verbonden.
- Server-URL
- Toevoegen
+ Breng je bibliotheek naar Nuvio met de add-ons die je gebruikt.
+ Voeg een add-on toe om je bibliotheek op te bouwen.
+ Maak Nuvio helemaal van jou.
+ Add-on-URL
+ Add-on toevoegen
+ Hier is nog niets
+ Je huidige add-ons hebben hier nog niets om te tonen.
Voeg een manifest-URL toe om catalogi, metadata, streams of ondertitels in Nuvio te laden.
Nog geen addons geïnstalleerd.
Voer een addon-URL in.
@@ -442,7 +444,9 @@
Download de laatste versie
Controleren op updates
Beheer addons en ontdekkingsbronnen.
+ Beheer add-ons en kies wat er in Nuvio verschijnt.
Beheer je gedownloade films en afleveringen.
+ Beheer wat je op dit apparaat hebt opgeslagen.
Downloads
ALGEMEEN
Beheer beschikbare integraties
@@ -656,7 +660,7 @@
HOME
BRONNEN
Installeer, verwijder, vernieuw en sorteer je contentbronnen.
- Verbind persoonlijke mediabronnen en beheer toegang tot je eigen bibliotheek.
+ Voeg de add-ons toe die je met Nuvio gebruikt en beheer ze.
Installeer JavaScript-scraperrepository's en test providers intern.
Pas startschermindeling, zichtbaarheid van inhoud en postergedrag aan
Instellingen voor de detail- en afleveringsschermen.
diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml
index c500dfcf3..473e6a21a 100644
--- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml
@@ -31,6 +31,13 @@
Niedostępny
Konfiguruj dodatek
Usuń dodatek
+ Przenieś swoją bibliotekę do Nuvio za pomocą używanych dodatków.
+ Dodaj dodatek, aby zacząć tworzyć swoją bibliotekę.
+ Dostosuj Nuvio do siebie.
+ URL dodatku
+ Dodaj dodatek
+ Na razie nic tu nie ma
+ Używane dodatki nie mają jeszcze nic do wyświetlenia.
Dodaj URL manifestu, aby zacząć ładować katalogi, metadane, strumienie lub napisy do Nuvio.
Brak zainstalowanych dodatków.
Wprowadź URL dodatku.
@@ -412,7 +419,9 @@
Sprawdź dostępność nowych wersji aplikacji.
Sprawdź aktualizacje
Zarządzaj dodatkami i źródłami odkrywania.
+ Zarządzaj dodatkami i wybierz, co ma być widoczne w Nuvio.
Zarządzaj pobranymi filmami i odcinkami.
+ Zarządzaj zawartością zapisaną na tym urządzeniu.
Pobrane
OGÓLNE
Połącz usługi TMDB i MDBList.
@@ -597,6 +606,7 @@
EKRAN GŁÓWNY
ŹRÓDŁA
Instaluj, usuwaj, odświeżaj i sortuj źródła treści.
+ Dodawaj używane w Nuvio dodatki i zarządzaj nimi.
Instaluj repozytoria scraperów JavaScript i testuj dostawców wewnętrznie.
Kontroluj, które katalogi pojawiają się na ekranie głównym i w jakiej kolejności.
Wyłącz sekcje szczegółów i zmień kolejność wszystkiego poniżej Hero.
diff --git a/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml b/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml
index 49fb2960d..97b5fde26 100644
--- a/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml
@@ -30,6 +30,13 @@
Indisponível
Configurar addon
Excluir addon
+ Leve sua biblioteca para o Nuvio com os addons que você usa.
+ Adicione um addon para começar a montar sua biblioteca.
+ Deixe o Nuvio do seu jeito.
+ URL do addon
+ Adicionar addon
+ Ainda não há nada aqui
+ Os addons que você usa ainda não têm nada para mostrar aqui.
Adicione uma URL de manifesto para começar a carregar catálogos, metadados, streams ou legendas no Nuvio.
Nenhum addon instalado ainda.
Insira uma URL de addon.
@@ -401,7 +408,9 @@
Baixar a versão mais recente
Verificar atualizações
Gerenciar addons e fontes de descoberta.
+ Gerencie addons e escolha o que aparece no Nuvio.
Gerencie seus filmes e episódios baixados.
+ Gerencie o que você salvou neste dispositivo.
Downloads
GERAL
Gerenciar integrações disponíveis
@@ -582,6 +591,7 @@
PÁGINA INICIAL
FONTES
Instale, remova, atualize e ordene suas fontes de conteúdo.
+ Adicione e gerencie os addons que você usa com o Nuvio.
Instale repositórios de scraper JavaScript e teste provedores internamente.
Ajuste layout da página inicial, visibilidade de conteúdo e comportamento de pôsteres
Configurações para as telas de detalhes e episódios.
diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings.xml b/composeApp/src/commonMain/composeResources/values-pt/strings.xml
index 51a4a7041..e6719a5f5 100644
--- a/composeApp/src/commonMain/composeResources/values-pt/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-pt/strings.xml
@@ -31,6 +31,13 @@
Indisponível
Configurar addon
Eliminar addon
+ Leva a tua biblioteca para o Nuvio com os addons que usas.
+ Adiciona um addon para começares a criar a tua biblioteca.
+ Deixa o Nuvio à tua maneira.
+ URL do addon
+ Adicionar addon
+ Ainda não há nada aqui
+ Os addons que usas ainda não têm nada para mostrar aqui.
Adiciona um URL de manifesto para começares a carregar catálogos, metadados, streams ou legendas no Nuvio.
Ainda não tens addons instalados.
Introduz o URL de um addon.
@@ -432,7 +439,9 @@
Transferir a versão mais recente
Procurar atualizações
Gere addons e fontes de descoberta.
+ Gere os addons e escolhe o que aparece no Nuvio.
Gere os teus filmes e episódios transferidos.
+ Gere o que guardaste neste dispositivo.
Transferências
GERAL
Gere as integrações disponíveis
@@ -627,6 +636,7 @@
PÁGINA INICIAL
FONTES
Instala, remove, atualiza e ordena as tuas fontes de conteúdo.
+ Adiciona e gere os addons que usas com o Nuvio.
Instala repositórios de scrapers JavaScript e testa fornecedores internamente.
Ajusta o esquema da página inicial, a visibilidade do conteúdo e o comportamento dos cartazes.
Definições para as páginas de detalhes e de episódios.
diff --git a/composeApp/src/commonMain/composeResources/values-ro/strings.xml b/composeApp/src/commonMain/composeResources/values-ro/strings.xml
index a26335699..f002ef425 100644
--- a/composeApp/src/commonMain/composeResources/values-ro/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ro/strings.xml
@@ -32,11 +32,13 @@
Indisponibil
Configurează extensia
Șterge extensia
- Conectează-ți propriul server media pentru a naviga și a reda din biblioteca personală.
- Adaugă un URL de server mai sus când vrei ca Nuvio să îți afișeze biblioteca privată.
- Nicio bibliotecă personală conectată.
- URL Server
- Adaugă
+ Folosește extensiile preferate pentru a-ți aduce biblioteca în Nuvio.
+ Adaugă o extensie pentru a începe să-ți construiești biblioteca.
+ Personalizează Nuvio.
+ URL extensie
+ Adaugă extensie
+ Nu este nimic aici încă
+ Extensiile pe care le folosești nu au încă nimic de afișat aici.
Adaugă un URL de manifest pentru a începe încărcarea de cataloage, metadate, fluxuri sau subtitrări în Nuvio.
Nu există extensii instalate încă.
Introdu un URL pentru extensie.
@@ -442,7 +444,9 @@
Descarcă cea mai recentă versiune
Caută actualizări
Gestionează extensiile și sursele de descoperire.
+ Gestionează extensiile și alege ce apare în Nuvio.
Gestionează filmele și episoadele descărcate.
+ Gestionează ce ai salvat pe acest dispozitiv.
Descărcări
GENERAL
Gestionează integrările disponibile
@@ -656,7 +660,7 @@
ECRAN PRINCIPAL
SURSE
Instalează, elimină, reîmprospătează și sortează sursele de conținut.
- Conectează surse media personale și gestionează accesul la propria bibliotecă.
+ Adaugă și gestionează extensiile pe care le folosești cu Nuvio.
Instalează depozite de scraper JavaScript și testează furnizorii intern.
Ajustează aspectul ecranului principal, vizibilitatea conținutului și comportamentul posterului
Setări pentru ecranele de detalii și de episoade.
@@ -1932,4 +1936,4 @@
- %d titlu
- %d titluri
-
\ No newline at end of file
+
diff --git a/composeApp/src/commonMain/composeResources/values-sk/strings.xml b/composeApp/src/commonMain/composeResources/values-sk/strings.xml
index f554f5310..9f1c75634 100644
--- a/composeApp/src/commonMain/composeResources/values-sk/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-sk/strings.xml
@@ -434,7 +434,9 @@
Stiahnuť najnovšie vydanie
Skontrolovať aktualizácie
Spravovať doplnky a zdroje objavovania.
+ Spravujte doplnky a vyberte, čo sa má zobrazovať v Nuviu.
Spravovať stiahnuté filmy a epizódy.
+ Spravujte obsah uložený v tomto zariadení.
Stiahnuté
VŠEOBECNÉ
Spravovať dostupné integrácie
@@ -1898,15 +1900,17 @@
Chyba torrentu: %1$s
Prepnúť
- Pripojte vlastný mediálny server na prehliadanie a prehrávanie z vašej osobnej knižnice.
- Pridajte URL servera vyššie, ak chcete, aby Nuvio zobrazovalo vašu súkromnú knižnicu.
- Nie sú pripojené žiadne osobné knižnice.
- URL servera
- Pridať
+ Preneste svoju knižnicu do Nuvia pomocou doplnkov, ktoré používate.
+ Pridajte doplnok a začnite si vytvárať knižnicu.
+ Prispôsobte si Nuvio.
+ URL doplnku
+ Pridať doplnok
+ Zatiaľ tu nič nie je
+ Doplnky, ktoré používate, tu zatiaľ nemajú čo zobraziť.
Registráciou súhlasím s
Podmienkami
Jazyk zariadenia
- Pripojte osobné mediálne zdroje a spravujte prístup k vlastnej knižnici.
+ Pridávajte a spravujte doplnky, ktoré používate s Nuviom.
Odosielať časové značky intra a outra
Odovzdať vyriešené časové značky intra a outra externému prehrávaču na automatické preskakovanie. Funguje iba v prehrávačoch, ktoré to podporujú; ostatné prehrávače to ignorujú.
Pôvodný jazyk
diff --git a/composeApp/src/commonMain/composeResources/values-tr/strings.xml b/composeApp/src/commonMain/composeResources/values-tr/strings.xml
index 1340bde98..7a6fb8396 100644
--- a/composeApp/src/commonMain/composeResources/values-tr/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-tr/strings.xml
@@ -31,6 +31,13 @@
Kullanılamıyor
Eklentiyi ayarla
Eklentiyi sil
+ Kullandığın eklentilerle kitaplığını Nuvio'ya taşı.
+ Kitaplığını oluşturmaya başlamak için bir eklenti ekle.
+ Nuvio'yu kendine göre ayarla.
+ Eklenti URL'si
+ Eklenti ekle
+ Burada henüz bir şey yok
+ Kullandığın eklentilerin burada gösterecek bir şeyi yok.
Nuvio'ya katalog, meta veri, yayın veya altyazı yüklemeye başlamak için bir manifest URL'si ekle.
Henüz eklenti kurulmamış.
Bir eklenti URL'si gir.
@@ -402,7 +409,9 @@
Uygulamanın yeni sürümü var mı kontrol et.
Güncellemeleri kontrol et
Eklentileri ve keşif kaynaklarını yönet.
+ Eklentileri yönet ve Nuvio’da nelerin görüneceğini seç.
İndirdiğin film ve bölümleri yönet.
+ Bu cihaza kaydettiklerini yönet.
İndirilenler
GENEL
TMDB ve MDBList servislerini bağla.
@@ -587,6 +596,7 @@
ANA SAYFA
KAYNAKLAR
İçerik kaynaklarını kur, kaldır, yenile ve sırala.
+ Nuvio ile kullandığın eklentileri ekle ve yönet.
JavaScript scraper depoları kur ve sağlayıcıları içeriden test et.
Ana sayfada hangi katalogların hangi sırayla görüneceğini kontrol et.
Detay bölümlerini kapat ve öne çıkanların altındaki her şeyi yeniden sırala.
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 77f6c9e63..d81ca03d1 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -32,11 +32,13 @@
Unavailable
Configure addon
Delete addon
- Connect your own media server to browse and play from your personal library.
- Add a server URL above when you want Nuvio to show your private library.
- No personal libraries connected.
- Server URL
- Add
+ Bring your library into Nuvio with the addons you use.
+ Add an addon to start building your library.
+ Make Nuvio yours.
+ Addon URL
+ Add Addon
+ Nothing here yet
+ Your current addons don’t have anything to show here.
Add a manifest URL to start loading catalogs, metadata, streams or subtitles into Nuvio.
No addons installed yet.
Enter an addon URL.
@@ -442,7 +444,9 @@
Download latest release
Check for updates
Manage addons and discovery sources.
+ Manage addons and choose what appears in Nuvio.
Manage your downloaded movies and episodes.
+ Manage what you’ve saved on this device.
Downloads
GENERAL
Manage available integrations
@@ -601,6 +605,37 @@
Visible
Hide value
Player, subtitles, and auto-play
+ Card Depth Effect
+ Adds a lit top edge and a soft sheen to image cards for a subtle sense of depth.
+ Enable depth effect
+ Edge glow
+ Subtle
+ Balanced
+ Bold
+ Top sheen
+ Off
+ Soft
+ Bright
+ Apply to
+ Fine-tune
+ Fine-tune Depth
+ Drag the dot right for a brighter edge, up for more sheen. Use the slider to extend the glow around the full outline.
+ Edge glow →
+ ↑ Sheen
+ Edge glow
+ Sheen
+ Edge coverage
+ Top only
+ Half
+ Full outline
+ Edge coverage
+ Episode Title
+ S1 · E1 · 45 min
+ Posters
+ Continue Watching
+ Episode cards
+ Cast
+ Trailers
Corner Radius
Poster Card Style
Width
@@ -656,7 +691,7 @@
HOME
SOURCES
Install, remove, refresh, and sort your content sources.
- Connect personal media sources and manage access to your own library.
+ Add and manage the addons you use with Nuvio.
Install JavaScript scraper repositories and test providers internally.
Adjust home layout, content visibility, and poster behavior
Settings for the detail and episode screens.
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/storage/LocalAccountDataCleaner.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/storage/LocalAccountDataCleaner.kt
index c3eff7ba8..68fa9f0af 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/storage/LocalAccountDataCleaner.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/storage/LocalAccountDataCleaner.kt
@@ -27,6 +27,7 @@ import com.nuvio.app.features.streams.StreamLaunchStore
import com.nuvio.app.features.streams.StreamsRepository
import com.nuvio.app.features.trakt.TraktAuthRepository
import com.nuvio.app.features.trakt.TraktSettingsRepository
+import com.nuvio.app.core.ui.CardDepthStyleRepository
import com.nuvio.app.core.ui.PosterCardStyleRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentCache
@@ -61,6 +62,7 @@ internal object LocalAccountDataCleaner {
CollectionRepository.clearLocalState()
ThemeSettingsRepository.clearLocalState()
PosterCardStyleRepository.clearLocalState()
+ CardDepthStyleRepository.clearLocalState()
TraktAuthRepository.clearLocalState()
TraktSettingsRepository.clearLocalState()
PlayerSettingsRepository.clearLocalState()
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt
index d1d566b7a..ad0774205 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt
@@ -17,6 +17,8 @@ import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepositor
import com.nuvio.app.features.player.PlayerSettingsStorage
import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.profiles.ProfileRepository
+import com.nuvio.app.core.ui.CardDepthStyleRepository
+import com.nuvio.app.core.ui.CardDepthStyleStorage
import com.nuvio.app.core.ui.PosterCardStyleRepository
import com.nuvio.app.core.ui.PosterCardStyleStorage
import com.nuvio.app.features.settings.ThemeSettingsStorage
@@ -27,7 +29,6 @@ import com.nuvio.app.features.tmdb.TmdbSettingsStorage
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
import com.nuvio.app.features.trakt.TraktCommentsStorage
import com.nuvio.app.features.trakt.TraktCommentsSettings
-import com.nuvio.app.features.trakt.ProfileSettingsWatchSourceOutbox
import com.nuvio.app.features.trakt.TraktSettingsStorage
import com.nuvio.app.features.trakt.TraktSettingsRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
@@ -35,21 +36,16 @@ import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepositor
import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.postgrest.rpc
import kotlin.concurrent.Volatile
-import kotlinx.atomicfu.locks.SynchronizedObject
-import kotlinx.atomicfu.locks.synchronized
-import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -65,22 +61,10 @@ import kotlinx.serialization.json.put
private const val PUSH_DEBOUNCE_MS = 1500L
-private data class ObservedProfileSettingsChange(
- val signature: String,
- val accountId: String?,
-)
-
-private data class SkippedProfileSettingsPush(
- val signature: String,
- val accountId: String?,
- val profileId: Int,
-)
-
object ProfileSettingsSync {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val log = Logger.withTag("ProfileSettingsSync")
private val syncMutex = Mutex()
- private val observeLock = SynchronizedObject()
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
@@ -93,62 +77,33 @@ object ProfileSettingsSync {
private var isServerSyncInFlight: Boolean = false
@Volatile
- private var skipNextPush: SkippedProfileSettingsPush? = null
-
- @Volatile
- private var pushEnabledAccountId: String? = null
-
- @Volatile
- private var pendingLocalPush: SkippedProfileSettingsPush? = null
+ private var skipNextPushSignature: String? = null
private var observeJob: Job? = null
- private var pendingPushRetryJob: Job? = null
- fun startObserving() = synchronized(observeLock) {
- if (observeJob?.isActive == true) return@synchronized
+ fun startObserving() {
+ if (observeJob?.isActive == true) return
ensureRepositoriesLoaded()
observeLocalChangesAndPush()
}
fun clearAccountState() {
- synchronized(observeLock) {
- observeJob?.cancel()
- observeJob = null
- }
- skipNextPush = null
- pushEnabledAccountId = null
- pendingLocalPush = null
- pendingPushRetryJob?.cancel()
- pendingPushRetryJob = null
+ observeJob?.cancel()
+ observeJob = null
+ skipNextPushSignature = null
}
suspend fun pull(profileId: Int): Boolean {
- startObserving()
- val accountId = currentCloudAccountId() ?: return false
+ ensureRepositoriesLoaded()
return syncMutex.withLock {
- if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) {
+ if (ProfileRepository.activeProfileId != profileId) {
log.d { "pull(profileId=$profileId) — skipped because profile is no longer active" }
return@withLock false
}
isServerSyncInFlight = true
try {
- pushEnabledAccountId = accountId
- hydrateDurableWatchSourcePush(profileId = profileId, accountId = accountId)
- if (
- !pushCurrentStateLocked(
- profileId = profileId,
- accountId = accountId,
- forceCurrentState = false,
- )
- ) {
- schedulePendingPushRetry()
- return@withLock false
- }
- val observedSignatureAtStart = currentObservedStateSignature()
val localBlob = exportSettingsBlob()
- if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) {
- throw CancellationException("Profile settings pull target changed")
- }
+ if (ProfileRepository.activeProfileId != profileId) return@withLock false
val localSignature = buildSignature(localBlob)
val params = buildJsonObject {
@@ -156,105 +111,41 @@ object ProfileSettingsSync {
put("p_platform", MOBILE_SYNC_PLATFORM)
}
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_profile_settings_blob", params)
- if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) {
- throw CancellationException("Profile settings pull target changed")
- }
+ if (ProfileRepository.activeProfileId != profileId) return@withLock false
val response = result.decodeList().firstOrNull()
val remoteJson = response?.settingsJson
- val pendingDuringPull = pendingLocalPush?.let { pending ->
- pending.accountId == accountId && pending.profileId == profileId
- } == true
- val durableSourceChangeDuringPull =
- ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId) != null
- if (
- pendingDuringPull ||
- durableSourceChangeDuringPull ||
- currentObservedStateSignature() != observedSignatureAtStart
- ) {
- if (
- !pushCurrentStateLocked(
- profileId = profileId,
- accountId = accountId,
- forceCurrentState = true,
- )
- ) {
- schedulePendingPushRetry()
- }
- return@withLock false
- }
-
if (remoteJson == null) {
log.i { "pull(profileId=$profileId) — no remote settings blob found" }
- if (localSignature != defaultSignature()) {
- pushToRemoteLocked(profileId, localBlob, accountId)
- }
- pushEnabledAccountId = accountId
return@withLock false
}
- val remoteBlob = try {
- json.decodeFromJsonElement(MobileProfileSettingsBlob.serializer(), remoteJson)
- } catch (error: Throwable) {
- log.e(error) { "pull(profileId=$profileId) — failed to decode remote settings blob" }
- throw error
- }
-
- var restoredPendingSourceAfterRemoteApply = false
isApplyingRemoteBlob = true
try {
+ val remoteBlob = runCatching {
+ json.decodeFromJsonElement(MobileProfileSettingsBlob.serializer(), remoteJson)
+ }.getOrElse { error ->
+ log.e(error) { "pull(profileId=$profileId) — failed to decode remote settings blob" }
+ return@withLock false
+ }
val remoteSignature = buildSignature(remoteBlob)
if (remoteSignature == localSignature) {
log.d { "pull(profileId=$profileId) — remote matches local" }
- pushEnabledAccountId = accountId
return@withLock false
}
- if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) {
- throw CancellationException("Profile settings pull target changed")
- }
+ if (ProfileRepository.activeProfileId != profileId) return@withLock false
applyRemoteBlob(remoteBlob)
- ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId)?.let { pendingSource ->
- if (TraktSettingsRepository.uiState.value.watchProgressSource != pendingSource.source) {
- TraktSettingsRepository.setWatchProgressSource(pendingSource.source, profileId)
- }
- restoredPendingSourceAfterRemoteApply = true
- }
- skipNextPush = SkippedProfileSettingsPush(
- signature = currentObservedStateSignature(),
- accountId = currentCloudAccountId(),
- profileId = profileId,
- )
+ skipNextPushSignature = currentObservedStateSignature()
} finally {
isApplyingRemoteBlob = false
}
- if (restoredPendingSourceAfterRemoteApply) {
- pendingLocalPush = SkippedProfileSettingsPush(
- signature = currentObservedStateSignature(),
- accountId = accountId,
- profileId = profileId,
- )
- if (
- !pushCurrentStateLocked(
- profileId = profileId,
- accountId = accountId,
- forceCurrentState = true,
- )
- ) {
- schedulePendingPushRetry()
- }
- return@withLock false
- }
-
log.i { "pull(profileId=$profileId) — applied remote settings blob" }
- pushEnabledAccountId = accountId
true
- } catch (error: CancellationException) {
- throw error
} catch (error: Exception) {
log.e(error) { "pull(profileId=$profileId) — FAILED" }
- throw error
+ false
} finally {
isServerSyncInFlight = false
}
@@ -263,22 +154,16 @@ object ProfileSettingsSync {
suspend fun pushCurrentProfileToRemote(): Boolean {
ensureRepositoriesLoaded()
- val accountId = currentCloudAccountId() ?: return false
return syncMutex.withLock {
- try {
+ runCatching {
val profileId = ProfileRepository.activeProfileId
- if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) return@withLock false
- pushCurrentStateLocked(
- profileId = profileId,
- accountId = accountId,
- forceCurrentState = true,
- )
- } catch (error: CancellationException) {
- throw error
- } catch (error: Throwable) {
+ val blob = exportSettingsBlob()
+ if (ProfileRepository.activeProfileId != profileId) return@runCatching false
+ pushToRemoteLocked(profileId, blob)
+ true
+ }.onFailure { error ->
log.e(error) { "pushCurrentProfileToRemote() — FAILED" }
- false
- }
+ }.getOrDefault(false)
}
}
@@ -289,6 +174,7 @@ object ProfileSettingsSync {
ThemeSettingsRepository.amoledEnabled.map { "amoled" },
ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.map { "liquid_glass_tab_bar" },
PosterCardStyleRepository.uiState.map { "poster_card_style" },
+ CardDepthStyleRepository.uiState.map { "card_depth_style" },
PlayerSettingsRepository.uiState.map { "player" },
StreamBadgeSettingsRepository.uiState.map { "stream_badges" },
DebridSettingsRepository.uiState.map { "debrid" },
@@ -303,84 +189,24 @@ object ProfileSettingsSync {
)
observeJob = scope.launch {
- combine(signatureFlows) {
- ObservedProfileSettingsChange(
- signature = currentObservedStateSignature(),
- accountId = currentCloudAccountId(),
- )
- }
+ combine(signatureFlows) { currentObservedStateSignature() }
.drop(1)
.distinctUntilChanged()
- .onEach { change ->
- val authState = AuthRepository.state.value
- if (authState !is AuthState.Authenticated || authState.isAnonymous) return@onEach
- if (change.accountId == null || change.accountId != authState.userId) return@onEach
- val observedChange = SkippedProfileSettingsPush(
- signature = change.signature,
- accountId = change.accountId,
- profileId = ProfileRepository.activeProfileId,
- )
- if (skipNextPush != observedChange) {
- pendingLocalPush = observedChange
- schedulePendingPushRetry()
- }
- }
.debounce(PUSH_DEBOUNCE_MS)
- .collect { change ->
+ .collect { signature ->
val authState = AuthRepository.state.value
if (authState !is AuthState.Authenticated || authState.isAnonymous) return@collect
- if (change.accountId == null || change.accountId != authState.userId) return@collect
- val profileId = ProfileRepository.activeProfileId
- val observedChange = SkippedProfileSettingsPush(
- signature = change.signature,
- accountId = change.accountId,
- profileId = profileId,
- )
- if (skipNextPush == observedChange) {
- skipNextPush = null
- if (pendingLocalPush == observedChange) {
- pendingLocalPush = null
- }
+ if (isApplyingRemoteBlob || isServerSyncInFlight) return@collect
+ if (signature == skipNextPushSignature) {
+ skipNextPushSignature = null
return@collect
}
- pendingLocalPush = observedChange
- if (pushEnabledAccountId != change.accountId) return@collect
- if (isApplyingRemoteBlob || isServerSyncInFlight) return@collect
pushCurrentProfileToRemote()
}
}
}
- private fun schedulePendingPushRetry() {
- if (pendingPushRetryJob?.isActive == true) return
- pendingPushRetryJob = scope.launch {
- var retryDelayMs = 5_000L
- while (pendingLocalPush != null) {
- delay(retryDelayMs)
- val pending = pendingLocalPush ?: break
- if (
- pending.accountId == currentCloudAccountId() &&
- pending.profileId == ProfileRepository.activeProfileId &&
- pushEnabledAccountId == pending.accountId &&
- !isApplyingRemoteBlob &&
- !isServerSyncInFlight &&
- pushCurrentProfileToRemote()
- ) {
- break
- }
- retryDelayMs = (retryDelayMs * 2L).coerceAtMost(60_000L)
- }
- }
- }
-
- private suspend fun pushToRemoteLocked(
- profileId: Int,
- blob: MobileProfileSettingsBlob,
- accountId: String,
- ) {
- if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) {
- throw CancellationException("Profile settings push target changed")
- }
+ private suspend fun pushToRemoteLocked(profileId: Int, blob: MobileProfileSettingsBlob) {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_platform", MOBILE_SYNC_PLATFORM)
@@ -388,80 +214,16 @@ object ProfileSettingsSync {
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_profile_settings_blob", params)
- if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) {
- throw CancellationException("Profile settings push target changed")
- }
log.d { "pushToRemoteLocked(profileId=$profileId) — success" }
}
- private fun hydrateDurableWatchSourcePush(profileId: Int, accountId: String) {
- val durableChange = ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId) ?: return
- if (TraktSettingsRepository.uiState.value.watchProgressSource != durableChange.source) {
- TraktSettingsRepository.setWatchProgressSource(durableChange.source, profileId)
- }
- pendingLocalPush = SkippedProfileSettingsPush(
- signature = currentObservedStateSignature(),
- accountId = accountId,
- profileId = profileId,
- )
- schedulePendingPushRetry()
- }
-
- private suspend fun pushCurrentStateLocked(
- profileId: Int,
- accountId: String,
- forceCurrentState: Boolean,
- ): Boolean {
- val durableChange = ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId)
- val inMemoryChange = pendingLocalPush?.takeIf { pending ->
- pending.accountId == accountId && pending.profileId == profileId
- }
- if (!forceCurrentState && durableChange == null && inMemoryChange == null) return true
-
- val signature = currentObservedStateSignature()
- pushToRemoteLocked(profileId, exportSettingsBlob(), accountId)
- if (currentObservedStateSignature() != signature) {
- pendingLocalPush = SkippedProfileSettingsPush(
- signature = currentObservedStateSignature(),
- accountId = accountId,
- profileId = profileId,
- )
- schedulePendingPushRetry()
- return false
- }
-
- val pushedChange = SkippedProfileSettingsPush(
- signature = signature,
- accountId = accountId,
- profileId = profileId,
- )
- if (pendingLocalPush == pushedChange) {
- pendingLocalPush = null
- }
- if (
- durableChange != null &&
- TraktSettingsRepository.uiState.value.watchProgressSource == durableChange.source
- ) {
- ProfileSettingsWatchSourceOutbox.clearIfMatches(durableChange)
- }
-
- val durablePushRemains = ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId) != null
- val memoryPushRemains = pendingLocalPush?.let { pending ->
- pending.accountId == accountId && pending.profileId == profileId
- } == true
- if (durablePushRemains || memoryPushRemains) {
- schedulePendingPushRetry()
- return false
- }
- return true
- }
-
private fun exportSettingsBlob(): MobileProfileSettingsBlob {
ensureRepositoriesLoaded()
return MobileProfileSettingsBlob(
features = MobileProfileSettingsFeatures(
themeSettings = ThemeSettingsStorage.exportToSyncPayload(),
posterCardStyleSettingsPayload = PosterCardStyleStorage.loadPayload().orEmpty().trim(),
+ cardDepthStyleSettingsPayload = CardDepthStyleStorage.loadPayload().orEmpty().trim(),
playerSettings = PlayerSettingsStorage.exportToSyncPayload(),
streamBadgeSettings = StreamBadgeSettingsStorage.exportToSyncPayload(),
debridSettings = DebridSettingsStorage.exportToSyncPayload(),
@@ -486,6 +248,9 @@ object ProfileSettingsSync {
PosterCardStyleStorage.savePayload(blob.features.posterCardStyleSettingsPayload)
PosterCardStyleRepository.onProfileChanged()
+ CardDepthStyleStorage.savePayload(blob.features.cardDepthStyleSettingsPayload)
+ CardDepthStyleRepository.onProfileChanged()
+
PlayerSettingsStorage.replaceFromSyncPayload(blob.features.playerSettings)
PlayerSettingsRepository.onProfileChanged()
@@ -523,6 +288,7 @@ object ProfileSettingsSync {
private fun ensureRepositoriesLoaded() {
ThemeSettingsRepository.ensureLoaded()
PosterCardStyleRepository.ensureLoaded()
+ CardDepthStyleRepository.ensureLoaded()
PlayerSettingsRepository.ensureLoaded()
StreamBadgeSettingsRepository.ensureLoaded()
DebridSettingsRepository.ensureLoaded()
@@ -539,14 +305,12 @@ object ProfileSettingsSync {
private fun buildSignature(blob: MobileProfileSettingsBlob): String =
json.encodeToString(MobileProfileSettingsBlob.serializer(), blob)
- private fun defaultSignature(): String =
- buildSignature(MobileProfileSettingsBlob())
-
private fun currentObservedStateSignature(): String = listOf(
"theme=${ThemeSettingsRepository.selectedTheme.value.name}",
"amoled=${ThemeSettingsRepository.amoledEnabled.value}",
"liquid_glass_tab_bar=${ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.value}",
"poster_card_style=${PosterCardStyleRepository.uiState.value}",
+ "card_depth_style=${CardDepthStyleRepository.uiState.value}",
"player=${PlayerSettingsRepository.uiState.value}",
"stream_badges=${StreamBadgeSettingsRepository.uiState.value}",
"debrid=${DebridSettingsRepository.uiState.value}",
@@ -560,13 +324,6 @@ object ProfileSettingsSync {
"episode_release_alerts=${EpisodeReleaseNotificationsRepository.uiState.value.isEnabled}",
).joinToString(separator = "||")
- private fun currentCloudAccountId(): String? =
- (AuthRepository.state.value as? AuthState.Authenticated)
- ?.takeUnless { it.isAnonymous }
- ?.userId
-
- private fun isCurrentSyncTarget(profileId: Int, accountId: String): Boolean =
- ProfileRepository.activeProfileId == profileId && currentCloudAccountId() == accountId
}
@Serializable
@@ -579,6 +336,7 @@ private data class MobileProfileSettingsBlob(
private data class MobileProfileSettingsFeatures(
@SerialName("theme_settings") val themeSettings: JsonObject = JsonObject(emptyMap()),
@SerialName("poster_card_style_settings_payload") val posterCardStyleSettingsPayload: String = "",
+ @SerialName("card_depth_style_settings_payload") val cardDepthStyleSettingsPayload: String = "",
@SerialName("player_settings") val playerSettings: JsonObject = JsonObject(emptyMap()),
@SerialName("stream_badge_settings") val streamBadgeSettings: JsonObject = JsonObject(emptyMap()),
@SerialName("debrid_settings") val debridSettings: JsonObject = JsonObject(emptyMap()),
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthEffect.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthEffect.kt
new file mode 100644
index 000000000..ed9581ee7
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthEffect.kt
@@ -0,0 +1,96 @@
+package com.nuvio.app.core.ui
+
+import androidx.compose.foundation.border
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun rememberCardDepthStyleUiState(): CardDepthStyleUiState {
+ CardDepthStyleRepository.ensureLoaded()
+ val uiState by CardDepthStyleRepository.uiState.collectAsState()
+ return uiState
+}
+
+@Composable
+fun Modifier.nuvioCardDepth(
+ shape: Shape,
+ surface: NuvioCardDepthSurface,
+ fallbackBorderAlpha: Float = 0f,
+): Modifier {
+ val state = rememberCardDepthStyleUiState()
+ if (!state.isEnabledFor(surface)) {
+ return if (fallbackBorderAlpha > 0f) {
+ border(
+ width = 1.dp,
+ color = Color.White.copy(alpha = fallbackBorderAlpha),
+ shape = shape,
+ )
+ } else {
+ this
+ }
+ }
+
+ return cardDepthVisual(
+ shape = shape,
+ edgeStrength = state.edgeStrength.toFloat(),
+ sheenStrength = state.sheenStrength.toFloat(),
+ edgeCoverage = state.edgeCoverage.toFloat(),
+ )
+}
+
+fun Modifier.cardDepthVisual(
+ shape: Shape,
+ edgeStrength: Float,
+ sheenStrength: Float,
+ edgeCoverage: Float = DefaultCardDepthEdgeCoverage.toFloat(),
+): Modifier {
+ val edgeTop = edgeStrength.coerceIn(0f, 100f) / 100f
+ val sheen = sheenStrength.coerceIn(0f, 100f) / 100f
+ val coverage = edgeCoverage.coerceIn(0f, 100f) / 100f
+
+ val withEdge = if (edgeTop > 0f) {
+ border(
+ width = 1.dp,
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.White.copy(alpha = edgeTop),
+ Color.White.copy(alpha = edgeTop * (0.33f + 0.67f * coverage)),
+ Color.White.copy(alpha = edgeTop * coverage),
+ ),
+ ),
+ shape = shape,
+ )
+ } else {
+ this
+ }
+
+ return if (sheen > 0f) {
+ withEdge.drawWithContent {
+ drawContent()
+ val sheenHeight = size.height * 0.22f
+ if (sheenHeight > 0f) {
+ drawRect(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.White.copy(alpha = sheen),
+ Color.Transparent,
+ ),
+ startY = 0f,
+ endY = sheenHeight,
+ ),
+ size = Size(size.width, sheenHeight),
+ )
+ }
+ }
+ } else {
+ withEdge
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleRepository.kt
new file mode 100644
index 000000000..bb73c3059
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleRepository.kt
@@ -0,0 +1,175 @@
+package com.nuvio.app.core.ui
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+internal const val DefaultCardDepthEdgeStrength = 28
+internal const val DefaultCardDepthSheenStrength = 10
+internal const val DefaultCardDepthEdgeCoverage = 0
+
+enum class NuvioCardDepthSurface {
+ Posters,
+ ContinueWatching,
+ EpisodeCards,
+ Cast,
+ Trailers,
+}
+
+@Serializable
+private data class StoredCardDepthStylePreferences(
+ val enabled: Boolean = false,
+ val edgeStrength: Int = DefaultCardDepthEdgeStrength,
+ val sheenStrength: Int = DefaultCardDepthSheenStrength,
+ val edgeCoverage: Int = DefaultCardDepthEdgeCoverage,
+ val postersEnabled: Boolean = true,
+ val continueWatchingEnabled: Boolean = true,
+ val episodeCardsEnabled: Boolean = true,
+ val castEnabled: Boolean = true,
+ val trailersEnabled: Boolean = true,
+)
+
+data class CardDepthStyleUiState(
+ val enabled: Boolean = false,
+ val edgeStrength: Int = DefaultCardDepthEdgeStrength,
+ val sheenStrength: Int = DefaultCardDepthSheenStrength,
+ val edgeCoverage: Int = DefaultCardDepthEdgeCoverage,
+ val postersEnabled: Boolean = true,
+ val continueWatchingEnabled: Boolean = true,
+ val episodeCardsEnabled: Boolean = true,
+ val castEnabled: Boolean = true,
+ val trailersEnabled: Boolean = true,
+) {
+ fun isEnabledFor(surface: NuvioCardDepthSurface): Boolean =
+ enabled && isSurfaceEnabled(surface)
+
+ fun isSurfaceEnabled(surface: NuvioCardDepthSurface): Boolean =
+ when (surface) {
+ NuvioCardDepthSurface.Posters -> postersEnabled
+ NuvioCardDepthSurface.ContinueWatching -> continueWatchingEnabled
+ NuvioCardDepthSurface.EpisodeCards -> episodeCardsEnabled
+ NuvioCardDepthSurface.Cast -> castEnabled
+ NuvioCardDepthSurface.Trailers -> trailersEnabled
+ }
+}
+
+object CardDepthStyleRepository {
+ private val json = Json {
+ ignoreUnknownKeys = true
+ encodeDefaults = true
+ }
+
+ private val _uiState = MutableStateFlow(CardDepthStyleUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private var hasLoaded = false
+
+ fun ensureLoaded() {
+ if (hasLoaded) return
+ loadFromDisk()
+ }
+
+ fun onProfileChanged() {
+ loadFromDisk()
+ }
+
+ fun clearLocalState() {
+ hasLoaded = false
+ _uiState.value = CardDepthStyleUiState()
+ }
+
+ fun setEnabled(enabled: Boolean) {
+ update { it.copy(enabled = enabled) }
+ }
+
+ fun setEdgeStrength(strength: Int) {
+ update { it.copy(edgeStrength = strength.coerceIn(0, 100)) }
+ }
+
+ fun setSheenStrength(strength: Int) {
+ update { it.copy(sheenStrength = strength.coerceIn(0, 100)) }
+ }
+
+ fun setEdgeCoverage(coverage: Int) {
+ update { it.copy(edgeCoverage = coverage.coerceIn(0, 100)) }
+ }
+
+ fun setSurfaceEnabled(surface: NuvioCardDepthSurface, enabled: Boolean) {
+ update {
+ when (surface) {
+ NuvioCardDepthSurface.Posters -> it.copy(postersEnabled = enabled)
+ NuvioCardDepthSurface.ContinueWatching -> it.copy(continueWatchingEnabled = enabled)
+ NuvioCardDepthSurface.EpisodeCards -> it.copy(episodeCardsEnabled = enabled)
+ NuvioCardDepthSurface.Cast -> it.copy(castEnabled = enabled)
+ NuvioCardDepthSurface.Trailers -> it.copy(trailersEnabled = enabled)
+ }
+ }
+ }
+
+ fun resetToDefaults() {
+ ensureLoaded()
+ if (_uiState.value == CardDepthStyleUiState()) return
+ _uiState.value = CardDepthStyleUiState()
+ persist()
+ }
+
+ private fun update(transform: (CardDepthStyleUiState) -> CardDepthStyleUiState) {
+ ensureLoaded()
+ val next = transform(_uiState.value)
+ if (_uiState.value == next) return
+ _uiState.value = next
+ persist()
+ }
+
+ private fun loadFromDisk() {
+ hasLoaded = true
+
+ val payload = CardDepthStyleStorage.loadPayload().orEmpty().trim()
+ if (payload.isEmpty()) {
+ _uiState.value = CardDepthStyleUiState()
+ return
+ }
+
+ val stored = runCatching {
+ json.decodeFromString(payload)
+ }.getOrNull()
+
+ _uiState.value = if (stored != null) {
+ CardDepthStyleUiState(
+ enabled = stored.enabled,
+ edgeStrength = stored.edgeStrength.coerceIn(0, 100),
+ sheenStrength = stored.sheenStrength.coerceIn(0, 100),
+ edgeCoverage = stored.edgeCoverage.coerceIn(0, 100),
+ postersEnabled = stored.postersEnabled,
+ continueWatchingEnabled = stored.continueWatchingEnabled,
+ episodeCardsEnabled = stored.episodeCardsEnabled,
+ castEnabled = stored.castEnabled,
+ trailersEnabled = stored.trailersEnabled,
+ )
+ } else {
+ CardDepthStyleUiState()
+ }
+ }
+
+ private fun persist() {
+ CardDepthStyleStorage.savePayload(
+ json.encodeToString(
+ StoredCardDepthStylePreferences(
+ enabled = _uiState.value.enabled,
+ edgeStrength = _uiState.value.edgeStrength,
+ sheenStrength = _uiState.value.sheenStrength,
+ edgeCoverage = _uiState.value.edgeCoverage,
+ postersEnabled = _uiState.value.postersEnabled,
+ continueWatchingEnabled = _uiState.value.continueWatchingEnabled,
+ episodeCardsEnabled = _uiState.value.episodeCardsEnabled,
+ castEnabled = _uiState.value.castEnabled,
+ trailersEnabled = _uiState.value.trailersEnabled,
+ ),
+ ),
+ )
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.kt
new file mode 100644
index 000000000..055940f23
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.kt
@@ -0,0 +1,6 @@
+package com.nuvio.app.core.ui
+
+internal expect object CardDepthStyleStorage {
+ fun loadPayload(): String?
+ fun savePayload(payload: String)
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt
index f7b8aa6c4..df5f05de6 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt
@@ -148,6 +148,10 @@ fun NuvioPosterCard(
.aspectRatio(shape.aspectRatio)
.clip(cardShape)
.background(tokens.colors.surface)
+ .nuvioCardDepth(
+ shape = cardShape,
+ surface = NuvioCardDepthSurface.Posters,
+ )
.posterCardClickable(
onClick = onClick,
onLongClick = onLongClick,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt
index 306152a5e..34de39b12 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt
@@ -32,6 +32,8 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.compose.LocalPlatformContext
import coil3.request.ImageRequest
+import com.nuvio.app.core.ui.NuvioCardDepthSurface
+import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.features.details.MetaPerson
import com.nuvio.app.features.details.castAvatarSharedTransitionKey
import nuvio.composeapp.generated.resources.*
@@ -143,6 +145,10 @@ private fun CastItem(
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = CircleShape,
+ )
+ .nuvioCardDepth(
+ shape = CircleShape,
+ surface = NuvioCardDepthSurface.Cast,
),
contentAlignment = Alignment.Center,
) {
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt
index a6fdc974b..039e10bd6 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt
@@ -64,7 +64,9 @@ import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.i18n.localizedSeasonEpisodeCode
import com.nuvio.app.core.ui.NuvioAnimatedWatchedBadge
+import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioProgressBar
+import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.features.details.MetaDetails
import com.nuvio.app.features.details.MetaEpisodeCardStyle
@@ -670,10 +672,10 @@ private fun EpisodeHorizontalCard(
.height(metrics.cardHeight)
.clip(cardShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f))
- .border(
- width = 1.dp,
- color = Color.White.copy(alpha = 0.12f),
+ .nuvioCardDepth(
shape = cardShape,
+ surface = NuvioCardDepthSurface.EpisodeCards,
+ fallbackBorderAlpha = 0.12f,
)
.posterCardClickable(
onClick = onClick,
@@ -699,12 +701,12 @@ private fun EpisodeHorizontalCard(
.fillMaxSize()
.background(
Brush.verticalGradient(
- colors = listOf(
- Color.Transparent,
- Color.Black.copy(alpha = 0.10f),
- Color.Black.copy(alpha = 0.42f),
- Color.Black.copy(alpha = 0.78f),
- ),
+ 0f to Color.Transparent,
+ 0.42f to Color.Transparent,
+ 0.56f to Color.Black.copy(alpha = 0.20f),
+ 0.70f to Color.Black.copy(alpha = 0.45f),
+ 0.84f to Color.Black.copy(alpha = 0.68f),
+ 1f to Color.Black.copy(alpha = 0.92f),
),
),
)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt
index e9ef5fa82..5ca4fa96f 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt
@@ -37,6 +37,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
+import com.nuvio.app.core.ui.NuvioCardDepthSurface
+import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.features.details.MetaTrailer
import nuvio.composeapp.generated.resources.*
import nuvio.composeapp.generated.resources.detail_tab_trailer
@@ -193,6 +195,10 @@ private fun TrailerCard(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(cornerRadius))
+ .nuvioCardDepth(
+ shape = RoundedCornerShape(cornerRadius),
+ surface = NuvioCardDepthSurface.Trailers,
+ )
.clickable(onClick = onClick),
) {
AsyncImage(
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
index bb0951e3b..a906d0033 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt
@@ -48,7 +48,9 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.DisintegratingContainer
+import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioProgressBar
+import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.core.ui.NuvioShelfSection
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.PosterLandscapeAspectRatio
@@ -703,6 +705,10 @@ private fun ContinueWatchingCard(
.aspectRatio(PosterLandscapeAspectRatio)
.clip(RoundedCornerShape(cardMetrics.cornerRadius))
.background(MaterialTheme.colorScheme.surfaceVariant)
+ .nuvioCardDepth(
+ shape = RoundedCornerShape(cardMetrics.cornerRadius),
+ surface = NuvioCardDepthSurface.ContinueWatching,
+ )
.posterCardClickable(
onClick = onClick,
onLongClick = onLongClick,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ParentalGuideRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ParentalGuideRepository.kt
index 06fe2fb5a..59e99d3db 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ParentalGuideRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ParentalGuideRepository.kt
@@ -8,7 +8,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
-private const val PARENTAL_GUIDE_BASE_URL = "https://api.imdbapi.dev"
+private const val PARENTAL_GUIDE_BASE_URL = "https://api.tiffara.com"
private val imdbIdPattern = Regex("tt\\d+")
data class ParentalGuideResult(
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt
index cd727a95c..fd76b3fef 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt
@@ -13,6 +13,7 @@ import com.nuvio.app.features.downloads.DownloadsRepository
import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.HomeRepository
+import com.nuvio.app.core.ui.CardDepthStyleRepository
import com.nuvio.app.core.ui.PosterCardStyleRepository
import com.nuvio.app.features.library.LibraryRepository
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
@@ -162,6 +163,7 @@ object ProfileRepository {
}
ThemeSettingsRepository.onProfileChanged()
PosterCardStyleRepository.onProfileChanged()
+ CardDepthStyleRepository.onProfileChanged()
PlayerSettingsRepository.onProfileChanged()
StreamBadgeSettingsRepository.onProfileChanged()
P2pSettingsRepository.onProfileChanged()
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt
index f4f494d27..ccd3fdcfd 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt
@@ -2,8 +2,12 @@ package com.nuvio.app.features.settings
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -11,6 +15,8 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -18,23 +24,81 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Tune
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+import com.nuvio.app.core.ui.CardDepthStyleRepository
+import com.nuvio.app.core.ui.CardDepthStyleUiState
+import com.nuvio.app.core.ui.DefaultCardDepthEdgeCoverage
+import com.nuvio.app.core.ui.DefaultCardDepthEdgeStrength
+import com.nuvio.app.core.ui.DefaultCardDepthSheenStrength
import com.nuvio.app.core.ui.NuvioActionLabel
+import com.nuvio.app.core.ui.NuvioCardDepthSurface
+import com.nuvio.app.core.ui.NuvioModalBottomSheet
import com.nuvio.app.core.ui.PosterCardStyleRepository
import com.nuvio.app.core.ui.PosterCardStyleUiState
+import com.nuvio.app.core.ui.cardDepthVisual
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.action_reset
+import nuvio.composeapp.generated.resources.settings_card_depth_apply_to
+import nuvio.composeapp.generated.resources.settings_card_depth_description
+import nuvio.composeapp.generated.resources.settings_card_depth_edge
+import nuvio.composeapp.generated.resources.settings_card_depth_edge_balanced
+import nuvio.composeapp.generated.resources.settings_card_depth_edge_bold
+import nuvio.composeapp.generated.resources.settings_card_depth_edge_subtle
+import nuvio.composeapp.generated.resources.settings_card_depth_edge_value
+import nuvio.composeapp.generated.resources.settings_card_depth_edge_coverage
+import nuvio.composeapp.generated.resources.settings_card_depth_coverage_full
+import nuvio.composeapp.generated.resources.settings_card_depth_coverage_half
+import nuvio.composeapp.generated.resources.settings_card_depth_coverage_top
+import nuvio.composeapp.generated.resources.settings_card_depth_coverage_value
+import nuvio.composeapp.generated.resources.settings_card_depth_enabled
+import nuvio.composeapp.generated.resources.settings_card_depth_fine_tune
+import nuvio.composeapp.generated.resources.settings_card_depth_fine_tune_hint
+import nuvio.composeapp.generated.resources.settings_card_depth_fine_tune_title
+import nuvio.composeapp.generated.resources.settings_card_depth_pad_edge_axis
+import nuvio.composeapp.generated.resources.settings_card_depth_pad_sheen_axis
+import nuvio.composeapp.generated.resources.settings_card_depth_preview_meta
+import nuvio.composeapp.generated.resources.settings_card_depth_preview_title
+import nuvio.composeapp.generated.resources.settings_card_depth_sheen
+import nuvio.composeapp.generated.resources.settings_card_depth_sheen_value
+import nuvio.composeapp.generated.resources.settings_card_depth_sheen_bright
+import nuvio.composeapp.generated.resources.settings_card_depth_sheen_off
+import nuvio.composeapp.generated.resources.settings_card_depth_sheen_soft
+import nuvio.composeapp.generated.resources.settings_card_depth_surface_cast
+import nuvio.composeapp.generated.resources.settings_card_depth_surface_continue_watching
+import nuvio.composeapp.generated.resources.settings_card_depth_surface_episodes
+import nuvio.composeapp.generated.resources.settings_card_depth_surface_posters
+import nuvio.composeapp.generated.resources.settings_card_depth_surface_trailers
+import nuvio.composeapp.generated.resources.settings_card_depth_title
import nuvio.composeapp.generated.resources.settings_poster_card_radius
import nuvio.composeapp.generated.resources.settings_poster_card_style
import nuvio.composeapp.generated.resources.settings_poster_card_width
@@ -90,6 +154,451 @@ internal fun LazyListScope.posterCustomizationSettingsContent(
}
}
}
+ item {
+ CardDepthStyleRepository.ensureLoaded()
+ val cardDepthState by CardDepthStyleRepository.uiState.collectAsState()
+ SettingsSection(
+ title = stringResource(Res.string.settings_card_depth_title),
+ isTablet = isTablet,
+ actions = {
+ NuvioActionLabel(
+ text = stringResource(Res.string.action_reset),
+ onClick = CardDepthStyleRepository::resetToDefaults,
+ )
+ },
+ ) {
+ SettingsGroup(isTablet = isTablet) {
+ CardDepthStyleControls(
+ isTablet = isTablet,
+ uiState = cardDepthState,
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun CardDepthStyleControls(
+ isTablet: Boolean,
+ uiState: CardDepthStyleUiState,
+) {
+ var showFineTune by remember { mutableStateOf(false) }
+ val edgeOptions = listOf(
+ PresetOption(stringResource(Res.string.settings_card_depth_edge_subtle), 28),
+ PresetOption(stringResource(Res.string.settings_card_depth_edge_balanced), 42),
+ PresetOption(stringResource(Res.string.settings_card_depth_edge_bold), 56),
+ )
+ val sheenOptions = listOf(
+ PresetOption(stringResource(Res.string.settings_card_depth_sheen_off), 0),
+ PresetOption(stringResource(Res.string.settings_card_depth_sheen_soft), 10),
+ PresetOption(stringResource(Res.string.settings_card_depth_sheen_bright), 16),
+ )
+ val coverageOptions = listOf(
+ PresetOption(stringResource(Res.string.settings_card_depth_coverage_top), 0),
+ PresetOption(stringResource(Res.string.settings_card_depth_coverage_half), 50),
+ PresetOption(stringResource(Res.string.settings_card_depth_coverage_full), 100),
+ )
+ val surfaceRows = listOf(
+ stringResource(Res.string.settings_card_depth_surface_posters) to NuvioCardDepthSurface.Posters,
+ stringResource(Res.string.settings_card_depth_surface_continue_watching) to NuvioCardDepthSurface.ContinueWatching,
+ stringResource(Res.string.settings_card_depth_surface_episodes) to NuvioCardDepthSurface.EpisodeCards,
+ stringResource(Res.string.settings_card_depth_surface_cast) to NuvioCardDepthSurface.Cast,
+ stringResource(Res.string.settings_card_depth_surface_trailers) to NuvioCardDepthSurface.Trailers,
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = if (isTablet) 20.dp else 16.dp, vertical = if (isTablet) 18.dp else 16.dp),
+ verticalArrangement = Arrangement.spacedBy(14.dp),
+ ) {
+ Text(
+ text = stringResource(Res.string.settings_card_depth_description),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ PosterToggleRow(
+ title = stringResource(Res.string.settings_card_depth_enabled),
+ checked = uiState.enabled,
+ onCheckedChange = CardDepthStyleRepository::setEnabled,
+ )
+ if (uiState.enabled) {
+ PosterStyleOptionRow(
+ title = stringResource(Res.string.settings_card_depth_edge),
+ selectedValue = uiState.edgeStrength,
+ options = edgeOptions,
+ onSelected = CardDepthStyleRepository::setEdgeStrength,
+ )
+ PosterStyleOptionRow(
+ title = stringResource(Res.string.settings_card_depth_sheen),
+ selectedValue = uiState.sheenStrength,
+ options = sheenOptions,
+ onSelected = CardDepthStyleRepository::setSheenStrength,
+ )
+ PosterStyleOptionRow(
+ title = stringResource(Res.string.settings_card_depth_edge_coverage),
+ selectedValue = uiState.edgeCoverage,
+ options = coverageOptions,
+ onSelected = CardDepthStyleRepository::setEdgeCoverage,
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .clickable { showFineTune = true },
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = stringResource(Res.string.settings_card_depth_fine_tune),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.Medium,
+ )
+ Icon(
+ imageVector = Icons.Rounded.Tune,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ Text(
+ text = stringResource(Res.string.settings_card_depth_apply_to),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.SemiBold,
+ )
+ surfaceRows.forEach { (title, surface) ->
+ PosterToggleRow(
+ title = title,
+ checked = uiState.isSurfaceEnabled(surface),
+ onCheckedChange = { enabled ->
+ CardDepthStyleRepository.setSurfaceEnabled(surface, enabled)
+ },
+ )
+ }
+ }
+ }
+
+ if (showFineTune) {
+ CardDepthFineTuneSheet(
+ initialEdgeStrength = uiState.edgeStrength,
+ initialSheenStrength = uiState.sheenStrength,
+ initialEdgeCoverage = uiState.edgeCoverage,
+ onDismiss = { showFineTune = false },
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun CardDepthFineTuneSheet(
+ initialEdgeStrength: Int,
+ initialSheenStrength: Int,
+ initialEdgeCoverage: Int,
+ onDismiss: () -> Unit,
+) {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ var draftEdge by remember { mutableFloatStateOf(initialEdgeStrength.toFloat()) }
+ var draftSheen by remember { mutableFloatStateOf(initialSheenStrength.toFloat()) }
+ var draftCoverage by remember { mutableFloatStateOf(initialEdgeCoverage.toFloat()) }
+
+ fun commitDraft() {
+ CardDepthStyleRepository.setEdgeStrength(draftEdge.roundToInt())
+ CardDepthStyleRepository.setSheenStrength(draftSheen.roundToInt())
+ CardDepthStyleRepository.setEdgeCoverage(draftCoverage.roundToInt())
+ }
+
+ NuvioModalBottomSheet(
+ onDismissRequest = {
+ commitDraft()
+ onDismiss()
+ },
+ sheetState = sheetState,
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, bottom = 32.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = stringResource(Res.string.settings_card_depth_fine_tune_title),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.SemiBold,
+ )
+ NuvioActionLabel(
+ text = stringResource(Res.string.action_reset),
+ onClick = {
+ draftEdge = DefaultCardDepthEdgeStrength.toFloat()
+ draftSheen = DefaultCardDepthSheenStrength.toFloat()
+ draftCoverage = DefaultCardDepthEdgeCoverage.toFloat()
+ commitDraft()
+ },
+ )
+ }
+ Text(
+ text = stringResource(Res.string.settings_card_depth_fine_tune_hint),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ CardDepthPreviewCard(
+ edgeStrength = draftEdge,
+ sheenStrength = draftSheen,
+ edgeCoverage = draftCoverage,
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = "${stringResource(Res.string.settings_card_depth_edge_value)}: ${formatDepthPercentage(draftEdge)}",
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ Text(
+ text = "${stringResource(Res.string.settings_card_depth_sheen_value)}: ${formatDepthPercentage(draftSheen)}",
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ CardDepthTuningPad(
+ edgeStrength = draftEdge,
+ sheenStrength = draftSheen,
+ onChange = { edge, sheen ->
+ draftEdge = edge
+ draftSheen = sheen
+ },
+ onCommit = { edge, sheen ->
+ CardDepthStyleRepository.setEdgeStrength(edge.roundToInt())
+ CardDepthStyleRepository.setSheenStrength(sheen.roundToInt())
+ },
+ )
+ Text(
+ text = "${stringResource(Res.string.settings_card_depth_coverage_value)}: ${formatDepthPercentage(draftCoverage)}",
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ Slider(
+ value = draftCoverage,
+ onValueChange = { draftCoverage = it },
+ onValueChangeFinished = {
+ CardDepthStyleRepository.setEdgeCoverage(draftCoverage.roundToInt())
+ },
+ valueRange = 0f..100f,
+ colors = SliderDefaults.colors(
+ thumbColor = MaterialTheme.colorScheme.primary,
+ activeTrackColor = MaterialTheme.colorScheme.primary,
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
+}
+
+private fun formatDepthPercentage(value: Float): String {
+ val tenths = (value.coerceIn(0f, 100f) * 10f).roundToInt()
+ return "${tenths / 10}.${tenths % 10}%"
+}
+
+@Composable
+private fun CardDepthPreviewCard(
+ edgeStrength: Float,
+ sheenStrength: Float,
+ edgeCoverage: Float,
+) {
+ val shape = RoundedCornerShape(14.dp)
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(16f / 9f)
+ .clip(shape)
+ .background(
+ Brush.linearGradient(
+ colors = listOf(
+ Color(0xFF33415C),
+ Color(0xFF232D42),
+ Color(0xFF141A28),
+ ),
+ ),
+ )
+ .cardDepthVisual(
+ shape = shape,
+ edgeStrength = edgeStrength,
+ sheenStrength = sheenStrength,
+ edgeCoverage = edgeCoverage,
+ ),
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ Brush.verticalGradient(
+ 0f to Color.Transparent,
+ 0.42f to Color.Transparent,
+ 0.56f to Color.Black.copy(alpha = 0.20f),
+ 0.70f to Color.Black.copy(alpha = 0.45f),
+ 0.84f to Color.Black.copy(alpha = 0.68f),
+ 1f to Color.Black.copy(alpha = 0.92f),
+ ),
+ ),
+ )
+ Column(
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Text(
+ text = stringResource(Res.string.settings_card_depth_preview_meta),
+ style = MaterialTheme.typography.labelSmall,
+ color = Color.White.copy(alpha = 0.78f),
+ )
+ Text(
+ text = stringResource(Res.string.settings_card_depth_preview_title),
+ style = MaterialTheme.typography.titleMedium,
+ color = Color.White,
+ fontWeight = FontWeight.ExtraBold,
+ )
+ }
+ }
+}
+
+@Composable
+private fun CardDepthTuningPad(
+ edgeStrength: Float,
+ sheenStrength: Float,
+ onChange: (Float, Float) -> Unit,
+ onCommit: (Float, Float) -> Unit,
+) {
+ val maxEdge = 70f
+ val maxSheen = 25f
+ val shape = RoundedCornerShape(16.dp)
+ val thumbColor = MaterialTheme.colorScheme.primary
+ val gridColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)
+ val currentEdge by rememberUpdatedState(edgeStrength)
+ val currentSheen by rememberUpdatedState(sheenStrength)
+ val currentOnChange by rememberUpdatedState(onChange)
+ val currentOnCommit by rememberUpdatedState(onCommit)
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .clip(shape)
+ .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
+ .border(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
+ shape = shape,
+ )
+ .pointerInput(Unit) {
+ fun valuesAt(position: Offset): Pair {
+ val x = (position.x / size.width).coerceIn(0f, 1f)
+ val y = 1f - (position.y / size.height).coerceIn(0f, 1f)
+ return (x * maxEdge) to (y * maxSheen)
+ }
+ detectDragGestures(
+ onDragStart = { position ->
+ val (edge, sheen) = valuesAt(position)
+ currentOnChange(edge, sheen)
+ },
+ onDrag = { change, _ ->
+ change.consume()
+ val (edge, sheen) = valuesAt(change.position)
+ currentOnChange(edge, sheen)
+ },
+ onDragEnd = {
+ currentOnCommit(currentEdge, currentSheen)
+ },
+ onDragCancel = {
+ currentOnCommit(currentEdge, currentSheen)
+ },
+ )
+ }
+ .pointerInput(Unit) {
+ fun valuesAt(position: Offset): Pair {
+ val x = (position.x / size.width).coerceIn(0f, 1f)
+ val y = 1f - (position.y / size.height).coerceIn(0f, 1f)
+ return (x * maxEdge) to (y * maxSheen)
+ }
+ detectTapGestures { position ->
+ val (edge, sheen) = valuesAt(position)
+ currentOnChange(edge, sheen)
+ currentOnCommit(edge, sheen)
+ }
+ },
+ ) {
+ Canvas(modifier = Modifier.fillMaxSize()) {
+ for (step in 1..3) {
+ val x = size.width * step / 4f
+ val y = size.height * step / 4f
+ drawLine(
+ color = gridColor,
+ start = Offset(x, 0f),
+ end = Offset(x, size.height),
+ strokeWidth = 1.dp.toPx(),
+ )
+ drawLine(
+ color = gridColor,
+ start = Offset(0f, y),
+ end = Offset(size.width, y),
+ strokeWidth = 1.dp.toPx(),
+ )
+ }
+ val thumbX = size.width * (edgeStrength / maxEdge).coerceIn(0f, 1f)
+ val thumbY = size.height * (1f - (sheenStrength / maxSheen).coerceIn(0f, 1f))
+ drawLine(
+ color = thumbColor.copy(alpha = 0.35f),
+ start = Offset(thumbX, 0f),
+ end = Offset(thumbX, size.height),
+ strokeWidth = 1.dp.toPx(),
+ )
+ drawLine(
+ color = thumbColor.copy(alpha = 0.35f),
+ start = Offset(0f, thumbY),
+ end = Offset(size.width, thumbY),
+ strokeWidth = 1.dp.toPx(),
+ )
+ drawCircle(
+ color = thumbColor.copy(alpha = 0.25f),
+ radius = 16.dp.toPx(),
+ center = Offset(thumbX, thumbY),
+ )
+ drawCircle(
+ color = thumbColor,
+ radius = 9.dp.toPx(),
+ center = Offset(thumbX, thumbY),
+ )
+ drawCircle(
+ color = Color.White,
+ radius = 4.dp.toPx(),
+ center = Offset(thumbX, thumbY),
+ )
+ }
+ Text(
+ text = stringResource(Res.string.settings_card_depth_pad_sheen_axis),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier
+ .align(Alignment.TopStart)
+ .padding(10.dp),
+ )
+ Text(
+ text = stringResource(Res.string.settings_card_depth_pad_edge_axis),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(10.dp),
+ )
+ }
}
@OptIn(ExperimentalLayoutApi::class)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
index 95ade90ce..419d5567f 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
@@ -710,6 +710,23 @@ internal fun settingsSearchEntries(
)
}
+ val cardDepthSection = stringResource(Res.string.settings_card_depth_title)
+ listOf(
+ PlaybackSearchRow("card-depth-effect", cardDepthSection, stringResource(Res.string.settings_card_depth_description)),
+ PlaybackSearchRow("card-depth-edge", stringResource(Res.string.settings_card_depth_edge)),
+ PlaybackSearchRow("card-depth-sheen", stringResource(Res.string.settings_card_depth_sheen)),
+ ).forEach { row ->
+ addRow(
+ page = SettingsPage.PosterCustomization,
+ key = "poster-${row.key}",
+ title = row.title,
+ description = row.description,
+ pageLabel = posterStylePage,
+ section = cardDepthSection,
+ icon = Icons.Rounded.Tune,
+ )
+ }
+
val homeLayoutSection = stringResource(Res.string.settings_homescreen_section_hero)
listOf(
PlaybackSearchRow("home-hero", stringResource(Res.string.settings_homescreen_show_hero), stringResource(Res.string.settings_homescreen_show_hero_description)),
diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.ios.kt
new file mode 100644
index 000000000..3a52ff9c0
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.ios.kt
@@ -0,0 +1,15 @@
+package com.nuvio.app.core.ui
+
+import com.nuvio.app.core.storage.ProfileScopedKey
+import platform.Foundation.NSUserDefaults
+
+internal actual object CardDepthStyleStorage {
+ private const val payloadKey = "card_depth_style_payload"
+
+ actual fun loadPayload(): String? =
+ NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(payloadKey))
+
+ actual fun savePayload(payload: String) {
+ NSUserDefaults.standardUserDefaults.setObject(payload, forKey = ProfileScopedKey.of(payloadKey))
+ }
+}
diff --git a/enginefs b/enginefs
new file mode 160000
index 000000000..9f11922ae
--- /dev/null
+++ b/enginefs
@@ -0,0 +1 @@
+Subproject commit 9f11922ae092e4a7120a7e1bdabb0698df1eecb8
diff --git a/iosApp/Configuration/Version.xcconfig b/iosApp/Configuration/Version.xcconfig
index eacc6f18a..a472b4f51 100644
--- a/iosApp/Configuration/Version.xcconfig
+++ b/iosApp/Configuration/Version.xcconfig
@@ -1,3 +1,3 @@
-CURRENT_PROJECT_VERSION=94
-MARKETING_VERSION=0.2.22
+CURRENT_PROJECT_VERSION=95
+MARKETING_VERSION=0.2.23
diff --git a/stremiotorrernt/public/enginefs b/stremiotorrernt/public/enginefs
new file mode 160000
index 000000000..9f11922ae
--- /dev/null
+++ b/stremiotorrernt/public/enginefs
@@ -0,0 +1 @@
+Subproject commit 9f11922ae092e4a7120a7e1bdabb0698df1eecb8