Compare commits

...

4 commits

46 changed files with 1283 additions and 442 deletions

View file

@ -568,8 +568,6 @@
<string name="settings_homescreen_show_hero_description">Prikaži glavni vrtuljak na vrhu početnog zaslona.</string> <string name="settings_homescreen_show_hero_description">Prikaži glavni vrtuljak na vrhu početnog zaslona.</string>
<string name="layout_hide_unreleased">Sakrij neobjavljeni sadržaj</string> <string name="layout_hide_unreleased">Sakrij neobjavljeni sadržaj</string>
<string name="layout_hide_unreleased_sub">Sakrij filmove i serije koji još nisu objavljeni.</string> <string name="layout_hide_unreleased_sub">Sakrij filmove i serije koji još nisu objavljeni.</string>
<string name="settings_homescreen_hide_catalog_underline">Sakrij podcrtavanje u katalogu</string>
<string name="settings_homescreen_hide_catalog_underline_description">Uklonite istaknutu liniju ispod naziva kataloga i zbirki u cijeloj aplikaciji.</string>
<string name="settings_homescreen_summary">%1$d od %2$d vidljivih kataloga • Odabrano %3$d izvora za glavni odjeljak</string> <string name="settings_homescreen_summary">%1$d od %2$d vidljivih kataloga • Odabrano %3$d izvora za glavni odjeljak</string>
<string name="settings_homescreen_summary_hint">Otvorite katalog samo kada ga trebate preimenovati ili promijeniti njegov redoslijed.</string> <string name="settings_homescreen_summary_hint">Otvorite katalog samo kada ga trebate preimenovati ili promijeniti njegov redoslijed.</string>
<string name="settings_homescreen_visible">Vidljivo</string> <string name="settings_homescreen_visible">Vidljivo</string>

View file

@ -26,6 +26,7 @@ actual object AddonStorage {
private const val preferencesName = "nuvio_addons" private const val preferencesName = "nuvio_addons"
private const val addonUrlsKey = "installed_manifest_urls" private const val addonUrlsKey = "installed_manifest_urls"
private const val addonEnabledStatesKey = "installed_manifest_enabled_states" private const val addonEnabledStatesKey = "installed_manifest_enabled_states"
private const val addonNamesKey = "installed_manifest_names"
private var preferences: SharedPreferences? = null private var preferences: SharedPreferences? = null
@ -66,6 +67,24 @@ actual object AddonStorage {
?.putString("${addonEnabledStatesKey}_$profileId", payload) ?.putString("${addonEnabledStatesKey}_$profileId", payload)
?.apply() ?.apply()
} }
actual fun loadAddonNames(profileId: Int): Map<String, String> =
preferences
?.getString("${addonNamesKey}_$profileId", null)
.orEmpty()
.lineSequence()
.mapNotNull(::parseAddonNameLine)
.toMap()
actual fun saveAddonNames(profileId: Int, names: Map<String, String>) {
val payload = names.entries.joinToString(separator = "\n") { (url, name) ->
"$url\t$name"
}
preferences
?.edit()
?.putString("${addonNamesKey}_$profileId", payload)
?.apply()
}
} }
private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? { private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? {
@ -78,6 +97,12 @@ private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? {
return url to enabled return url to enabled
} }
private fun parseAddonNameLine(line: String): Pair<String, String>? {
val url = line.substringBefore("\t").trim().takeIf { it.isNotEmpty() } ?: return null
val name = line.substringAfter("\t", "").trim().takeIf { it.isNotEmpty() } ?: return null
return url to name
}
private val addonHttpClient = OkHttpClient.Builder() private val addonHttpClient = OkHttpClient.Builder()
.dns(IPv4FirstDns()) .dns(IPv4FirstDns())
.connectTimeout(60, TimeUnit.SECONDS) .connectTimeout(60, TimeUnit.SECONDS)

View file

@ -570,8 +570,6 @@
<string name="settings_homescreen_show_hero_description">Покажи въртележка в горната част на началото.</string> <string name="settings_homescreen_show_hero_description">Покажи въртележка в горната част на началото.</string>
<string name="layout_hide_unreleased">Скрий невипуснатото съдържание</string> <string name="layout_hide_unreleased">Скрий невипуснатото съдържание</string>
<string name="layout_hide_unreleased_sub">Скрий филми и сериали, които все още не са излезли.</string> <string name="layout_hide_unreleased_sub">Скрий филми и сериали, които все още не са излезли.</string>
<string name="settings_homescreen_hide_catalog_underline">Скрий подчертаването на каталога</string>
<string name="settings_homescreen_hide_catalog_underline_description">Премахни акцентната линия под заглавията на каталозите и колекциите в цялото приложение.</string>
<string name="settings_homescreen_summary">%1$d от %2$d каталога видими • %3$d основни извора избрани</string> <string name="settings_homescreen_summary">%1$d от %2$d каталога видими • %3$d основни извора избрани</string>
<string name="settings_homescreen_summary_hint">Отворете каталог само когато трябва да го преименувате или пренаредите.</string> <string name="settings_homescreen_summary_hint">Отворете каталог само когато трябва да го преименувате или пренаредите.</string>
<string name="settings_homescreen_visible">Видим</string> <string name="settings_homescreen_visible">Видим</string>

View file

@ -582,8 +582,6 @@
<string name="settings_homescreen_show_hero_description">Zobrazit hlavní karusel v horní části domovské stránky.</string> <string name="settings_homescreen_show_hero_description">Zobrazit hlavní karusel v horní části domovské stránky.</string>
<string name="layout_hide_unreleased">Skrýt nevydaný obsah</string> <string name="layout_hide_unreleased">Skrýt nevydaný obsah</string>
<string name="layout_hide_unreleased_sub">Skrýt filmy a pořady, které ještě neměly premiéru.</string> <string name="layout_hide_unreleased_sub">Skrýt filmy a pořady, které ještě neměly premiéru.</string>
<string name="settings_homescreen_hide_catalog_underline">Skrýt podtržení katalogu</string>
<string name="settings_homescreen_hide_catalog_underline_description">Odstranit barevnou linii pod názvy katalogů a sbírek v celé aplikaci.</string>
<string name="settings_homescreen_summary">%1$d z %2$d katalogů viditelných • %3$d zdrojů Hero sekce vybráno</string> <string name="settings_homescreen_summary">%1$d z %2$d katalogů viditelných • %3$d zdrojů Hero sekce vybráno</string>
<string name="settings_homescreen_summary_hint">Otevřete katalog pouze tehdy, když jej potřebujete přejmenovat nebo změnit jeho pořadí.</string> <string name="settings_homescreen_summary_hint">Otevřete katalog pouze tehdy, když jej potřebujete přejmenovat nebo změnit jeho pořadí.</string>
<string name="settings_homescreen_visible">Viditelné</string> <string name="settings_homescreen_visible">Viditelné</string>

View file

@ -1723,8 +1723,6 @@
<string name="settings_fusion_badges_empty">Δεν έχουν εισαχθεί URL badge Fusion.</string> <string name="settings_fusion_badges_empty">Δεν έχουν εισαχθεί URL badge Fusion.</string>
<string name="settings_fusion_badges_summary">%1$d/%2$d URLs, %3$d active Fusion badges</string> <string name="settings_fusion_badges_summary">%1$d/%2$d URLs, %3$d active Fusion badges</string>
<string name="settings_hide_secret">Απόκρυψη value</string> <string name="settings_hide_secret">Απόκρυψη value</string>
<string name="settings_homescreen_hide_catalog_underline">Απόκρυψη Catalog Underline</string>
<string name="settings_homescreen_hide_catalog_underline_description">Αφαίρεση της γραμμής τονισμού κάτω από τους τίτλους καταλόγων και συλλογών σε όλη την εφαρμογή.</string>
<string name="settings_integrations_debrid_description">Συνδέστε λογαριασμούς για συνδέσμους και πρόσβαση στη βιβλιοθήκη</string> <string name="settings_integrations_debrid_description">Συνδέστε λογαριασμούς για συνδέσμους και πρόσβαση στη βιβλιοθήκη</string>
<string name="settings_licenses_attributions_exoplayer_body">Χρησιμοποιείται για αναπαραγωγή σε εκδόσεις Android.</string> <string name="settings_licenses_attributions_exoplayer_body">Χρησιμοποιείται για αναπαραγωγή σε εκδόσεις Android.</string>
<string name="settings_licenses_attributions_exoplayer_license">Licensed under the Apache License, Έκδοση 2.0.</string> <string name="settings_licenses_attributions_exoplayer_license">Licensed under the Apache License, Έκδοση 2.0.</string>

View file

@ -582,8 +582,6 @@
<string name="settings_homescreen_show_hero_description">Mostrar un carrusel destacado en la parte superior del inicio. Elige hasta 2 catálogos de origen abajo.</string> <string name="settings_homescreen_show_hero_description">Mostrar un carrusel destacado en la parte superior del inicio. Elige hasta 2 catálogos de origen abajo.</string>
<string name="layout_hide_unreleased">Ocultar contenido inédito</string> <string name="layout_hide_unreleased">Ocultar contenido inédito</string>
<string name="layout_hide_unreleased_sub">Oculta películas y programas que aún no se han estrenado.</string> <string name="layout_hide_unreleased_sub">Oculta películas y programas que aún no se han estrenado.</string>
<string name="settings_homescreen_hide_catalog_underline">Ocultar subrayado del catálogo</string>
<string name="settings_homescreen_hide_catalog_underline_description">Elimine la línea de acento debajo de los títulos de catálogos y colecciones en toda la aplicación.</string>
<string name="settings_homescreen_summary">%1$d de %2$d catálogos visibles • %3$d fuentes de Destacado seleccionadas</string> <string name="settings_homescreen_summary">%1$d de %2$d catálogos visibles • %3$d fuentes de Destacado seleccionadas</string>
<string name="settings_homescreen_summary_hint">Abre un catálogo solo cuando necesites cambiarle el nombre o reordenarlo.</string> <string name="settings_homescreen_summary_hint">Abre un catálogo solo cuando necesites cambiarle el nombre o reordenarlo.</string>
<string name="settings_homescreen_visible">Visible</string> <string name="settings_homescreen_visible">Visible</string>

View file

@ -592,8 +592,6 @@
<string name="settings_homescreen_show_hero_description">Afficher un carrousel Hero en vedette en haut de laccueil. Choisissez jusquà 2 catalogues sources ci-dessous.</string> <string name="settings_homescreen_show_hero_description">Afficher un carrousel Hero en vedette en haut de laccueil. Choisissez jusquà 2 catalogues sources ci-dessous.</string>
<string name="layout_hide_unreleased">Masquer le contenu non sorti</string> <string name="layout_hide_unreleased">Masquer le contenu non sorti</string>
<string name="layout_hide_unreleased_sub">Masque les films et séries qui ne sont pas encore sortis.</string> <string name="layout_hide_unreleased_sub">Masque les films et séries qui ne sont pas encore sortis.</string>
<string name="settings_homescreen_hide_catalog_underline">Masquer le soulignement des catalogues</string>
<string name="settings_homescreen_hide_catalog_underline_description">Supprime la ligne daccent sous les titres de catalogues et de collections dans toute lapp.</string>
<string name="settings_homescreen_summary">%1$d sur %2$d catalogues visibles • %3$d sources Hero sélectionnées</string> <string name="settings_homescreen_summary">%1$d sur %2$d catalogues visibles • %3$d sources Hero sélectionnées</string>
<string name="settings_homescreen_summary_hint">Ouvrez un catalogue uniquement si vous avez besoin de le renommer ou de le réorganiser.</string> <string name="settings_homescreen_summary_hint">Ouvrez un catalogue uniquement si vous avez besoin de le renommer ou de le réorganiser.</string>
<string name="settings_homescreen_visible">Visible</string> <string name="settings_homescreen_visible">Visible</string>

View file

@ -525,8 +525,6 @@
<string name="settings_homescreen_show_hero_description">Kiemelt galéria (carousel) megjelenítése a kezdőlap tetején.</string> <string name="settings_homescreen_show_hero_description">Kiemelt galéria (carousel) megjelenítése a kezdőlap tetején.</string>
<string name="layout_hide_unreleased">Meg nem jelent tartalmak elrejtése</string> <string name="layout_hide_unreleased">Meg nem jelent tartalmak elrejtése</string>
<string name="layout_hide_unreleased_sub">A még meg nem jelent filmek és sorozatok elrejtése.</string> <string name="layout_hide_unreleased_sub">A még meg nem jelent filmek és sorozatok elrejtése.</string>
<string name="settings_homescreen_hide_catalog_underline">Katalógus aláhúzás elrejtése</string>
<string name="settings_homescreen_hide_catalog_underline_description">A kiemelő vonal eltávolítása a katalógusok és gyűjtemények címei alól az alkalmazás egészében.</string>
<string name="settings_homescreen_summary">%1$d / %2$d katalógus látható • %3$d kiemelt forrás kiválasztva</string> <string name="settings_homescreen_summary">%1$d / %2$d katalógus látható • %3$d kiemelt forrás kiválasztva</string>
<string name="settings_homescreen_summary_hint">Csak akkor nyiss meg egy katalógust, ha át szeretnéd nevezni vagy át szeretnéd rendezni.</string> <string name="settings_homescreen_summary_hint">Csak akkor nyiss meg egy katalógust, ha át szeretnéd nevezni vagy át szeretnéd rendezni.</string>
<string name="settings_homescreen_visible">Látható</string> <string name="settings_homescreen_visible">Látható</string>

View file

@ -547,8 +547,6 @@
<string name="settings_homescreen_show_hero_description">Tampilkan karousel hero di bagian atas beranda.</string> <string name="settings_homescreen_show_hero_description">Tampilkan karousel hero di bagian atas beranda.</string>
<string name="layout_hide_unreleased">Sembunyikan Konten Belum Rilis</string> <string name="layout_hide_unreleased">Sembunyikan Konten Belum Rilis</string>
<string name="layout_hide_unreleased_sub">Sembunyikan film dan serial yang belum dirilis.</string> <string name="layout_hide_unreleased_sub">Sembunyikan film dan serial yang belum dirilis.</string>
<string name="settings_homescreen_hide_catalog_underline">Sembunyikan Garis Bawah Katalog</string>
<string name="settings_homescreen_hide_catalog_underline_description">Hapus garis aksen di bawah judul katalog dan koleksi di seluruh aplikasi.</string>
<string name="settings_homescreen_summary">%1$d dari %2$d katalog terlihat • %3$d sumber hero dipilih</string> <string name="settings_homescreen_summary">%1$d dari %2$d katalog terlihat • %3$d sumber hero dipilih</string>
<string name="settings_homescreen_summary_hint">Buka katalog hanya jika perlu mengganti nama atau mengurutkannya.</string> <string name="settings_homescreen_summary_hint">Buka katalog hanya jika perlu mengganti nama atau mengurutkannya.</string>
<string name="settings_homescreen_visible">Terlihat</string> <string name="settings_homescreen_visible">Terlihat</string>

View file

@ -547,8 +547,6 @@
<string name="settings_homescreen_show_hero_description">Tampilkan karousel hero di bagian atas beranda.</string> <string name="settings_homescreen_show_hero_description">Tampilkan karousel hero di bagian atas beranda.</string>
<string name="layout_hide_unreleased">Sembunyikan Konten Belum Rilis</string> <string name="layout_hide_unreleased">Sembunyikan Konten Belum Rilis</string>
<string name="layout_hide_unreleased_sub">Sembunyikan film dan serial yang belum dirilis.</string> <string name="layout_hide_unreleased_sub">Sembunyikan film dan serial yang belum dirilis.</string>
<string name="settings_homescreen_hide_catalog_underline">Sembunyikan Garis Bawah Katalog</string>
<string name="settings_homescreen_hide_catalog_underline_description">Hapus garis aksen di bawah judul katalog dan koleksi di seluruh aplikasi.</string>
<string name="settings_homescreen_summary">%1$d dari %2$d katalog terlihat • %3$d sumber hero dipilih</string> <string name="settings_homescreen_summary">%1$d dari %2$d katalog terlihat • %3$d sumber hero dipilih</string>
<string name="settings_homescreen_summary_hint">Buka katalog hanya jika perlu mengganti nama atau mengurutkannya.</string> <string name="settings_homescreen_summary_hint">Buka katalog hanya jika perlu mengganti nama atau mengurutkannya.</string>
<string name="settings_homescreen_visible">Terlihat</string> <string name="settings_homescreen_visible">Terlihat</string>

View file

@ -1315,8 +1315,6 @@
<string name="settings_appearance_liquid_glass_description">Usa la barra dei pannelli nativa dell'iPhone su iOS 26 e versioni successive.</string> <string name="settings_appearance_liquid_glass_description">Usa la barra dei pannelli nativa dell'iPhone su iOS 26 e versioni successive.</string>
<string name="layout_hide_unreleased">Nascondi contenuti non rilasciati</string> <string name="layout_hide_unreleased">Nascondi contenuti non rilasciati</string>
<string name="layout_hide_unreleased_sub">Nascondi i film e le serie TV che non sono ancora stati rilasciati.</string> <string name="layout_hide_unreleased_sub">Nascondi i film e le serie TV che non sono ancora stati rilasciati.</string>
<string name="settings_homescreen_hide_catalog_underline">Nascondi sottolineatura catalogo</string>
<string name="settings_homescreen_hide_catalog_underline_description">Rimuove la linea di evidenziazione sotto i titoli dei cataloghi e delle collezioni in tutta l'app.</string>
<string name="settings_hide_secret">Nascondi valore</string> <string name="settings_hide_secret">Nascondi valore</string>
<string name="settings_show_secret">Mostra valore</string> <string name="settings_show_secret">Mostra valore</string>
<string name="settings_continue_watching_blur_next_up_description">Sfoca le miniature del prossimo episodio in Inizia a guardare per evitare spoiler.</string> <string name="settings_continue_watching_blur_next_up_description">Sfoca le miniature del prossimo episodio in Inizia a guardare per evitare spoiler.</string>

View file

@ -582,8 +582,6 @@
<string name="settings_homescreen_show_hero_description">ホーム上部にヒーローカルーセルを表示します。</string> <string name="settings_homescreen_show_hero_description">ホーム上部にヒーローカルーセルを表示します。</string>
<string name="layout_hide_unreleased">未公開コンテンツを非表示</string> <string name="layout_hide_unreleased">未公開コンテンツを非表示</string>
<string name="layout_hide_unreleased_sub">まだ公開されていない映画やシリーズを非表示にします。</string> <string name="layout_hide_unreleased_sub">まだ公開されていない映画やシリーズを非表示にします。</string>
<string name="settings_homescreen_hide_catalog_underline">カタログの下線を非表示</string>
<string name="settings_homescreen_hide_catalog_underline_description">アプリ全体のカタログ・コレクションタイトル下のアクセントラインを非表示にします。</string>
<string name="settings_homescreen_summary">%1$d / %2$d カタログ表示中 • %3$d ヒーローソース選択中</string> <string name="settings_homescreen_summary">%1$d / %2$d カタログ表示中 • %3$d ヒーローソース選択中</string>
<string name="settings_homescreen_summary_hint">カタログは名前の変更や並べ替えが必要なときのみ開いてください。</string> <string name="settings_homescreen_summary_hint">カタログは名前の変更や並べ替えが必要なときのみ開いてください。</string>
<string name="settings_homescreen_visible">表示</string> <string name="settings_homescreen_visible">表示</string>

View file

@ -531,8 +531,6 @@
<string name="settings_homescreen_show_hero_description">Vis hero-karusell øverst på hjemskjermen.</string> <string name="settings_homescreen_show_hero_description">Vis hero-karusell øverst på hjemskjermen.</string>
<string name="layout_hide_unreleased">Skjul ikke-utgitt innhold</string> <string name="layout_hide_unreleased">Skjul ikke-utgitt innhold</string>
<string name="layout_hide_unreleased_sub">Skjul filmer og serier som ikke er utgitt ennå.</string> <string name="layout_hide_unreleased_sub">Skjul filmer og serier som ikke er utgitt ennå.</string>
<string name="settings_homescreen_hide_catalog_underline">Skjul katalog-understrek</string>
<string name="settings_homescreen_hide_catalog_underline_description">Fjern aksentlinjen under katalog- og samlingstitler.</string>
<string name="settings_homescreen_summary">%1$d av %2$d kataloger synlige • %3$d hero-kilder valgt</string> <string name="settings_homescreen_summary">%1$d av %2$d kataloger synlige • %3$d hero-kilder valgt</string>
<string name="settings_homescreen_summary_hint">Åpne en katalog kun når du skal gi nytt navn eller endre rekkefølge.</string> <string name="settings_homescreen_summary_hint">Åpne en katalog kun når du skal gi nytt navn eller endre rekkefølge.</string>
<string name="settings_homescreen_visible">Synlig</string> <string name="settings_homescreen_visible">Synlig</string>

View file

@ -600,8 +600,6 @@
<string name="settings_homescreen_show_hero_description">Toon herocarrousel bovenaan de startpagina.</string> <string name="settings_homescreen_show_hero_description">Toon herocarrousel bovenaan de startpagina.</string>
<string name="layout_hide_unreleased">Niet-uitgebrachte inhoud verbergen</string> <string name="layout_hide_unreleased">Niet-uitgebrachte inhoud verbergen</string>
<string name="layout_hide_unreleased_sub">Verberg films en series die nog niet zijn uitgebracht.</string> <string name="layout_hide_unreleased_sub">Verberg films en series die nog niet zijn uitgebracht.</string>
<string name="settings_homescreen_hide_catalog_underline">Catalogusonderstreping verbergen</string>
<string name="settings_homescreen_hide_catalog_underline_description">Verwijder de accentlijn onder catalogus- en collectietitels in de hele app.</string>
<string name="settings_homescreen_summary">%1$d van %2$d catalogi zichtbaar • %3$d herobronnen geselecteerd</string> <string name="settings_homescreen_summary">%1$d van %2$d catalogi zichtbaar • %3$d herobronnen geselecteerd</string>
<string name="settings_homescreen_summary_hint">Open een catalogus alleen als je deze wilt hernoemen of herordenen.</string> <string name="settings_homescreen_summary_hint">Open een catalogus alleen als je deze wilt hernoemen of herordenen.</string>
<string name="settings_homescreen_visible">Zichtbaar</string> <string name="settings_homescreen_visible">Zichtbaar</string>

View file

@ -547,8 +547,6 @@
<string name="settings_homescreen_show_hero_description">Wyświetl wyróżnioną karuzelę hero na górze ekranu głównego. Wybierz do 2 katalogów źródłowych poniżej.</string> <string name="settings_homescreen_show_hero_description">Wyświetl wyróżnioną karuzelę hero na górze ekranu głównego. Wybierz do 2 katalogów źródłowych poniżej.</string>
<string name="layout_hide_unreleased">Ukryj niewydane treści</string> <string name="layout_hide_unreleased">Ukryj niewydane treści</string>
<string name="layout_hide_unreleased_sub">Ukryj filmy i seriale, które nie zostały jeszcze wydane.</string> <string name="layout_hide_unreleased_sub">Ukryj filmy i seriale, które nie zostały jeszcze wydane.</string>
<string name="settings_homescreen_hide_catalog_underline">Ukryj podkreślenie katalogu</string>
<string name="settings_homescreen_hide_catalog_underline_description">Usuń linię akcentu pod tytułami katalogów i kolekcji w całej aplikacji.</string>
<string name="settings_homescreen_summary">%1$d z %2$d katalogów widocznych • %3$d źródeł hero wybranych</string> <string name="settings_homescreen_summary">%1$d z %2$d katalogów widocznych • %3$d źródeł hero wybranych</string>
<string name="settings_homescreen_summary_hint">Otwórz katalog tylko wtedy, gdy chcesz zmienić jego nazwę lub kolejność.</string> <string name="settings_homescreen_summary_hint">Otwórz katalog tylko wtedy, gdy chcesz zmienić jego nazwę lub kolejność.</string>
<string name="settings_homescreen_visible">Widoczne</string> <string name="settings_homescreen_visible">Widoczne</string>

View file

@ -532,8 +532,6 @@
<string name="settings_homescreen_show_hero_description">Exibir carrossel hero no topo da página inicial.</string> <string name="settings_homescreen_show_hero_description">Exibir carrossel hero no topo da página inicial.</string>
<string name="layout_hide_unreleased">Ocultar Conteúdo Não Lançado</string> <string name="layout_hide_unreleased">Ocultar Conteúdo Não Lançado</string>
<string name="layout_hide_unreleased_sub">Ocultar filmes e séries que ainda não foram lançados.</string> <string name="layout_hide_unreleased_sub">Ocultar filmes e séries que ainda não foram lançados.</string>
<string name="settings_homescreen_hide_catalog_underline">Ocultar Linha Sublinhada do Catálogo</string>
<string name="settings_homescreen_hide_catalog_underline_description">Remover a linha de destaque abaixo dos títulos de catálogo e coleção em todo o app.</string>
<string name="settings_homescreen_summary">%1$d de %2$d catálogos visíveis • %3$d fontes de hero selecionadas</string> <string name="settings_homescreen_summary">%1$d de %2$d catálogos visíveis • %3$d fontes de hero selecionadas</string>
<string name="settings_homescreen_summary_hint">Abra um catálogo apenas quando precisar renomeá-lo ou reordená-lo.</string> <string name="settings_homescreen_summary_hint">Abra um catálogo apenas quando precisar renomeá-lo ou reordená-lo.</string>
<string name="settings_homescreen_visible">Visível</string> <string name="settings_homescreen_visible">Visível</string>

View file

@ -576,8 +576,6 @@
<string name="settings_homescreen_show_hero_description">Apresenta o carrossel de destaques no topo da página inicial.</string> <string name="settings_homescreen_show_hero_description">Apresenta o carrossel de destaques no topo da página inicial.</string>
<string name="layout_hide_unreleased">Ocultar conteúdo não lançado</string> <string name="layout_hide_unreleased">Ocultar conteúdo não lançado</string>
<string name="layout_hide_unreleased_sub">Ocultar filmes e séries que ainda não foram lançados.</string> <string name="layout_hide_unreleased_sub">Ocultar filmes e séries que ainda não foram lançados.</string>
<string name="settings_homescreen_hide_catalog_underline">Ocultar sublinhado dos catálogos</string>
<string name="settings_homescreen_hide_catalog_underline_description">Remove a linha de destaque sob os títulos de catálogos e coleções em toda a app.</string>
<string name="settings_homescreen_summary">%1$d de %2$d catálogos visíveis • %3$d fontes de destaque selecionadas</string> <string name="settings_homescreen_summary">%1$d de %2$d catálogos visíveis • %3$d fontes de destaque selecionadas</string>
<string name="settings_homescreen_summary_hint">Abre um catálogo apenas quando precisares de o renomear ou reordenar.</string> <string name="settings_homescreen_summary_hint">Abre um catálogo apenas quando precisares de o renomear ou reordenar.</string>
<string name="settings_homescreen_visible">Visível</string> <string name="settings_homescreen_visible">Visível</string>

View file

@ -600,8 +600,6 @@
<string name="settings_homescreen_show_hero_description">Afișează un carusel în partea de sus a ecranului principal.</string> <string name="settings_homescreen_show_hero_description">Afișează un carusel în partea de sus a ecranului principal.</string>
<string name="layout_hide_unreleased">Ascunde conținutul nelansat</string> <string name="layout_hide_unreleased">Ascunde conținutul nelansat</string>
<string name="layout_hide_unreleased_sub">Ascunde filmele și serialele care nu au fost lansate încă.</string> <string name="layout_hide_unreleased_sub">Ascunde filmele și serialele care nu au fost lansate încă.</string>
<string name="settings_homescreen_hide_catalog_underline">Ascunde sublinierea catalogului</string>
<string name="settings_homescreen_hide_catalog_underline_description">Elimină linia de accent de sub titlurile cataloagelor și colecțiilor din întreaga aplicație.</string>
<string name="settings_homescreen_summary">%1$d din %2$d cataloage vizibile • %3$d surse hero selectate</string> <string name="settings_homescreen_summary">%1$d din %2$d cataloage vizibile • %3$d surse hero selectate</string>
<string name="settings_homescreen_summary_hint">Deschide un catalog doar când trebuie să îl redenumești sau să îl rearanjezi.</string> <string name="settings_homescreen_summary_hint">Deschide un catalog doar când trebuie să îl redenumești sau să îl rearanjezi.</string>
<string name="settings_homescreen_visible">Vizibil</string> <string name="settings_homescreen_visible">Vizibil</string>

View file

@ -571,8 +571,6 @@
<string name="settings_homescreen_show_hero_description">Zobraziť hero karusel v hornej časti domovskej obrazovky.</string> <string name="settings_homescreen_show_hero_description">Zobraziť hero karusel v hornej časti domovskej obrazovky.</string>
<string name="layout_hide_unreleased">Skryť nevydaný obsah</string> <string name="layout_hide_unreleased">Skryť nevydaný obsah</string>
<string name="layout_hide_unreleased_sub">Skryť filmy a seriály, ktoré ešte neboli vydané.</string> <string name="layout_hide_unreleased_sub">Skryť filmy a seriály, ktoré ešte neboli vydané.</string>
<string name="settings_homescreen_hide_catalog_underline">Skryť podčiarknutie katalógu</string>
<string name="settings_homescreen_hide_catalog_underline_description">Odstrániť zvýrazňovací riadok pod názvami katalógov a kolekcií v celej aplikácii.</string>
<string name="settings_homescreen_summary">%1$d z %2$d katalógov viditeľných • %3$d zdrojov hero sekcie vybraných</string> <string name="settings_homescreen_summary">%1$d z %2$d katalógov viditeľných • %3$d zdrojov hero sekcie vybraných</string>
<string name="settings_homescreen_summary_hint">Otvorte katalóg iba vtedy, keď ho potrebujete premenovať alebo zmeniť poradie.</string> <string name="settings_homescreen_summary_hint">Otvorte katalóg iba vtedy, keď ho potrebujete premenovať alebo zmeniť poradie.</string>
<string name="settings_homescreen_visible">Viditeľné</string> <string name="settings_homescreen_visible">Viditeľné</string>

View file

@ -537,8 +537,6 @@
<string name="settings_homescreen_show_hero_description">Ana sayfanın üstünde öne çıkan bir kaydırmalı alan göster. Aşağıdan en fazla 2 kaynak katalog seç.</string> <string name="settings_homescreen_show_hero_description">Ana sayfanın üstünde öne çıkan bir kaydırmalı alan göster. Aşağıdan en fazla 2 kaynak katalog seç.</string>
<string name="layout_hide_unreleased">Henüz Yayınlanmamış İçerikleri Gizle</string> <string name="layout_hide_unreleased">Henüz Yayınlanmamış İçerikleri Gizle</string>
<string name="layout_hide_unreleased_sub">Henüz yayınlanmamış film ve dizileri gizle.</string> <string name="layout_hide_unreleased_sub">Henüz yayınlanmamış film ve dizileri gizle.</string>
<string name="settings_homescreen_hide_catalog_underline">Katalog Alt Çizgisini Gizle</string>
<string name="settings_homescreen_hide_catalog_underline_description">Uygulama genelinde katalog ve koleksiyon başlıklarının altındaki vurgu çizgisini kaldır.</string>
<string name="settings_homescreen_summary">%1$d / %2$d katalog görünür • %3$d öne çıkan kaynak seçili</string> <string name="settings_homescreen_summary">%1$d / %2$d katalog görünür • %3$d öne çıkan kaynak seçili</string>
<string name="settings_homescreen_summary_hint">Bir kataloğu yalnızca adını değiştirmek veya sıralamak gerektiğinde aç.</string> <string name="settings_homescreen_summary_hint">Bir kataloğu yalnızca adını değiştirmek veya sıralamak gerektiğinde aç.</string>
<string name="settings_homescreen_visible">Görünür</string> <string name="settings_homescreen_visible">Görünür</string>

View file

@ -579,8 +579,6 @@
<string name="settings_homescreen_show_hero_description">Hiển thị Hero ở đầu Trang chủ</string> <string name="settings_homescreen_show_hero_description">Hiển thị Hero ở đầu Trang chủ</string>
<string name="layout_hide_unreleased">Ẩn nội dung chưa phát hành</string> <string name="layout_hide_unreleased">Ẩn nội dung chưa phát hành</string>
<string name="layout_hide_unreleased_sub">Ẩn phim và chương trình chưa phát hành</string> <string name="layout_hide_unreleased_sub">Ẩn phim và chương trình chưa phát hành</string>
<string name="settings_homescreen_hide_catalog_underline">Ẩn gạch chân danh mục</string>
<string name="settings_homescreen_hide_catalog_underline_description">Ẩn gạch chân dưới tiêu đề danh mục và bộ sưu tập</string>
<string name="settings_homescreen_summary">%1$d/%2$d danh mục • %3$d nguồn Hero</string> <string name="settings_homescreen_summary">%1$d/%2$d danh mục • %3$d nguồn Hero</string>
<string name="settings_homescreen_summary_hint">Chỉ mở danh mục khi cần đổi tên hoặc sắp xếp</string> <string name="settings_homescreen_summary_hint">Chỉ mở danh mục khi cần đổi tên hoặc sắp xếp</string>
<string name="settings_homescreen_visible">Hiển thị</string> <string name="settings_homescreen_visible">Hiển thị</string>

View file

@ -56,9 +56,24 @@
<string name="addons_overview_addons">Addons</string> <string name="addons_overview_addons">Addons</string>
<string name="addons_overview_catalogs">Catalogs</string> <string name="addons_overview_catalogs">Catalogs</string>
<string name="addons_refresh">Refresh addon</string> <string name="addons_refresh">Refresh addon</string>
<string name="addons_refresh_all">Refresh Addons</string>
<string name="addons_refresh_all_description">Pull latest addon changes for the current profile.</string>
<string name="addons_refresh_all_running">Refreshing addons...</string>
<string name="addons_refresh_all_complete">Refreshed %1$d addons and %2$d manifests.</string>
<string name="addons_refresh_all_partial">Updated %1$d addons. %2$d manifests refreshed and %3$d failed.</string>
<string name="addons_refresh_all_partial_warning">Refresh incomplete: %1$s</string>
<string name="addons_refresh_all_conflict">The profile or local addon state changed during refresh. Your local state was kept; refresh again to pull the latest profile.</string>
<string name="addons_refresh_all_failed">Refresh failed: %1$s</string>
<string name="addons_refresh_error_default">Unable to refresh addons.</string>
<string name="addons_refresh_error_manifest_timeout">Manifest refresh timed out.</string>
<string name="addons_refresh_error_pending_upload">Pending local addon changes could not be uploaded.</string>
<string name="addons_refresh_error_sync">Unable to load addons from Nuvio Sync.</string>
<string name="addons_refresh_error_sync_timeout">Nuvio Sync did not respond before the refresh timed out.</string>
<string name="addons_refresh_warning_pending_upload">Local addon changes are saved on this device but could not be uploaded yet.</string>
<string name="addons_section_add_addon">Add Addon</string> <string name="addons_section_add_addon">Add Addon</string>
<string name="addons_section_installed">Installed Addons</string> <string name="addons_section_installed">Installed Addons</string>
<string name="addons_section_overview">Overview</string> <string name="addons_section_overview">Overview</string>
<string name="addons_section_sync">Sync</string>
<string name="addons_summary_id_rules">%1$d id rules</string> <string name="addons_summary_id_rules">%1$d id rules</string>
<string name="addons_version_format">Version %1$s</string> <string name="addons_version_format">Version %1$s</string>
<string name="cd_selected">Selected</string> <string name="cd_selected">Selected</string>
@ -609,8 +624,6 @@
<string name="layout_hide_unreleased_sub">Hide movies and shows that haven't been released yet.</string> <string name="layout_hide_unreleased_sub">Hide movies and shows that haven't been released yet.</string>
<string name="layout_catalog_type">Show Catalog Type</string> <string name="layout_catalog_type">Show Catalog Type</string>
<string name="layout_catalog_type_sub">Show type suffix next to catalog name (Movie/Series).</string> <string name="layout_catalog_type_sub">Show type suffix next to catalog name (Movie/Series).</string>
<string name="settings_homescreen_hide_catalog_underline">Hide Catalog Underline</string>
<string name="settings_homescreen_hide_catalog_underline_description">Remove the accent line under catalog and collection titles throughout the app.</string>
<string name="settings_homescreen_summary">%1$d of %2$d catalogs visible • %3$d hero sources selected</string> <string name="settings_homescreen_summary">%1$d of %2$d catalogs visible • %3$d hero sources selected</string>
<string name="settings_homescreen_summary_hint">Open a catalog only when you need to rename or reorder it.</string> <string name="settings_homescreen_summary_hint">Open a catalog only when you need to rename or reorder it.</string>
<string name="settings_homescreen_visible">Visible</string> <string name="settings_homescreen_visible">Visible</string>

View file

@ -68,7 +68,6 @@ fun <T> NuvioShelfSection(
rowContentPadding: PaddingValues = PaddingValues(0.dp), rowContentPadding: PaddingValues = PaddingValues(0.dp),
rowModifier: Modifier = Modifier, rowModifier: Modifier = Modifier,
itemSpacing: Dp = 10.dp, itemSpacing: Dp = 10.dp,
showHeaderAccent: Boolean = true,
onViewAllClick: (() -> Unit)? = null, onViewAllClick: (() -> Unit)? = null,
viewAllPillSize: NuvioViewAllPillSize = NuvioViewAllPillSize.Default, viewAllPillSize: NuvioViewAllPillSize = NuvioViewAllPillSize.Default,
key: ((T) -> Any)? = null, key: ((T) -> Any)? = null,
@ -85,7 +84,6 @@ fun <T> NuvioShelfSection(
NuvioShelfSectionHeader( NuvioShelfSectionHeader(
title = title, title = title,
modifier = Modifier.padding(horizontal = headerHorizontalPadding), modifier = Modifier.padding(horizontal = headerHorizontalPadding),
showAccent = showHeaderAccent,
onViewAllClick = onViewAllClick, onViewAllClick = onViewAllClick,
viewAllPillSize = viewAllPillSize, viewAllPillSize = viewAllPillSize,
) )
@ -244,7 +242,6 @@ fun NuvioPosterCard(
private fun NuvioShelfSectionHeader( private fun NuvioShelfSectionHeader(
title: String, title: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showAccent: Boolean = true,
onViewAllClick: (() -> Unit)? = null, onViewAllClick: (() -> Unit)? = null,
viewAllPillSize: NuvioViewAllPillSize = NuvioViewAllPillSize.Default, viewAllPillSize: NuvioViewAllPillSize = NuvioViewAllPillSize.Default,
) { ) {
@ -278,18 +275,6 @@ private fun NuvioShelfSectionHeader(
modifier = viewAllPlaceholderModifier, modifier = viewAllPlaceholderModifier,
) )
} }
if (showAccent) {
Box(
modifier = Modifier
.padding(top = NuvioTokens.Space.s6)
.width(NuvioTokens.Space.s64 - NuvioTokens.Space.s4)
.height(NuvioTokens.Space.s4)
.background(
color = tokens.colors.accent,
shape = tokens.shapes.chip,
),
)
}
} }
} }

View file

@ -67,8 +67,30 @@ data class ManagedAddon(
data class AddonsUiState( data class AddonsUiState(
val addons: List<ManagedAddon> = emptyList(), val addons: List<ManagedAddon> = emptyList(),
val refreshState: AddonRefreshState = AddonRefreshState.Idle,
val refreshRevision: Long = 0L,
) )
sealed interface AddonRefreshState {
data object Idle : AddonRefreshState
data object Refreshing : AddonRefreshState
data class Complete(
val addonCount: Int,
val refreshedManifestCount: Int,
) : AddonRefreshState
data class Partial(
val addonCount: Int,
val refreshedManifestCount: Int,
val failedManifestCount: Int,
val warningMessage: String? = null,
) : AddonRefreshState
data object Conflict : AddonRefreshState
data class Failed(val message: String) : AddonRefreshState
}
data class AddonOverview( data class AddonOverview(
val totalAddons: Int, val totalAddons: Int,
val activeAddons: Int, val activeAddons: Int,

View file

@ -5,6 +5,8 @@ internal expect object AddonStorage {
fun saveInstalledAddonUrls(profileId: Int, urls: List<String>) fun saveInstalledAddonUrls(profileId: Int, urls: List<String>)
fun loadAddonEnabledStates(profileId: Int): Map<String, Boolean> fun loadAddonEnabledStates(profileId: Int): Map<String, Boolean>
fun saveAddonEnabledStates(profileId: Int, states: Map<String, Boolean>) fun saveAddonEnabledStates(profileId: Int, states: Map<String, Boolean>)
fun loadAddonNames(profileId: Int): Map<String, String>
fun saveAddonNames(profileId: Int, names: Map<String, String>)
} }
data class RawHttpResponse( data class RawHttpResponse(

View file

@ -0,0 +1,61 @@
package com.nuvio.app.features.addons
internal data class RemoteAddonValue(
val manifestUrl: String,
val userSetName: String?,
val enabled: Boolean,
)
internal fun canApplyRemoteAddonSnapshot(
currentProfileId: Int,
snapshotProfileId: Int,
currentMutationRevision: Long,
expectedMutationRevision: Long,
hasPendingPush: Boolean,
): Boolean =
currentProfileId == snapshotProfileId &&
currentMutationRevision == expectedMutationRevision &&
!hasPendingPush
internal fun mergeRemoteAddonSnapshot(
existingAddons: List<ManagedAddon>,
remoteAddons: List<RemoteAddonValue>,
forceManifestRefresh: Boolean,
): List<ManagedAddon> {
val existingByUrl = existingAddons.associateBy(ManagedAddon::manifestUrl)
return remoteAddons.map { remote ->
val existing = existingByUrl[remote.manifestUrl]
val normalizedName = remote.userSetName?.takeIf(String::isNotBlank)
val merged = when {
existing == null -> ManagedAddon(
manifestUrl = remote.manifestUrl,
isRefreshing = remote.enabled,
userSetName = normalizedName,
enabled = remote.enabled,
)
existing.manifest != null -> existing.copy(
manifestUrl = remote.manifestUrl,
isRefreshing = false,
userSetName = normalizedName,
enabled = remote.enabled,
)
existing.isRefreshing -> existing.copy(
manifestUrl = remote.manifestUrl,
userSetName = normalizedName,
enabled = remote.enabled,
)
else -> existing.copy(
manifestUrl = remote.manifestUrl,
isRefreshing = remote.enabled,
errorMessage = null,
userSetName = normalizedName,
enabled = remote.enabled,
)
}
when {
!merged.enabled -> merged.copy(isRefreshing = false)
forceManifestRefresh -> merged.copy(isRefreshing = true, errorMessage = null)
else -> merged
}
}
}

View file

@ -105,6 +105,16 @@ internal fun AddonsSettingsPageContent(
SectionHeader(stringResource(Res.string.addons_section_overview)) SectionHeader(stringResource(Res.string.addons_section_overview))
OverviewCard(overview = overview) OverviewCard(overview = overview)
SectionHeader(stringResource(Res.string.addons_section_sync))
RefreshAddonsCard(
refreshState = uiState.refreshState,
onRefreshClick = {
coroutineScope.launch {
AddonRepository.refreshAll()
}
},
)
SectionHeader(stringResource(Res.string.addons_section_add_addon)) SectionHeader(stringResource(Res.string.addons_section_add_addon))
AddAddonCard( AddAddonCard(
addonUrl = addonUrl, addonUrl = addonUrl,
@ -220,6 +230,54 @@ private fun SectionHeader(text: String) {
NuvioSectionLabel(text = text) NuvioSectionLabel(text = text)
} }
@Composable
private fun RefreshAddonsCard(
refreshState: AddonRefreshState,
onRefreshClick: () -> Unit,
) {
val isRefreshing = refreshState is AddonRefreshState.Refreshing
val statusText = when (refreshState) {
AddonRefreshState.Idle -> stringResource(Res.string.addons_refresh_all_description)
AddonRefreshState.Refreshing -> stringResource(Res.string.addons_refresh_all_running)
is AddonRefreshState.Complete -> stringResource(
Res.string.addons_refresh_all_complete,
refreshState.addonCount,
refreshState.refreshedManifestCount,
)
is AddonRefreshState.Partial -> refreshState.warningMessage?.let { warning ->
stringResource(Res.string.addons_refresh_all_partial_warning, warning)
} ?: stringResource(
Res.string.addons_refresh_all_partial,
refreshState.addonCount,
refreshState.refreshedManifestCount,
refreshState.failedManifestCount,
)
AddonRefreshState.Conflict -> stringResource(Res.string.addons_refresh_all_conflict)
is AddonRefreshState.Failed -> stringResource(
Res.string.addons_refresh_all_failed,
refreshState.message,
)
}
NuvioSurfaceCard {
Text(
text = statusText,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(16.dp))
NuvioPrimaryButton(
text = if (isRefreshing) {
stringResource(Res.string.addons_refresh_all_running)
} else {
stringResource(Res.string.addons_refresh_all)
},
enabled = !isRefreshing,
onClick = onRefreshClick,
)
}
}
@Composable @Composable
private fun OverviewCard(overview: AddonOverview) { private fun OverviewCard(overview: AddonOverview) {
NuvioSurfaceCard { NuvioSurfaceCard {

View file

@ -43,7 +43,6 @@ fun DetailPosterRailSection(
horizontal = headerHorizontalPadding + horizontalScrollPadding, horizontal = headerHorizontalPadding + horizontalScrollPadding,
), ),
rowModifier = Modifier.nuvioHorizontalScrollBleed(horizontalScrollPadding), rowModifier = Modifier.nuvioHorizontalScrollBleed(horizontalScrollPadding),
showHeaderAccent = false,
key = { item -> item.stableKey() }, key = { item -> item.stableKey() },
) { item -> ) { item ->
HomePosterCard( HomePosterCard(

View file

@ -34,7 +34,6 @@ data class HomeCatalogSettingsUiState(
val heroEnabled: Boolean = true, val heroEnabled: Boolean = true,
val showCatalogType: Boolean = true, val showCatalogType: Boolean = true,
val hideUnreleasedContent: Boolean = false, val hideUnreleasedContent: Boolean = false,
val hideCatalogUnderline: Boolean = false,
val items: List<HomeCatalogSettingsItem> = emptyList(), val items: List<HomeCatalogSettingsItem> = emptyList(),
) { ) {
val signature: String val signature: String
@ -45,8 +44,6 @@ data class HomeCatalogSettingsUiState(
append('|') append('|')
append(hideUnreleasedContent) append(hideUnreleasedContent)
append('|') append('|')
append(hideCatalogUnderline)
append('|')
append( append(
items.joinToString(separator = "|") { item -> items.joinToString(separator = "|") { item ->
"${item.key}:${item.order}:${item.enabled}:${item.heroSourceEnabled}:${item.customTitle}" "${item.key}:${item.order}:${item.enabled}:${item.heroSourceEnabled}:${item.customTitle}"
@ -66,7 +63,6 @@ internal data class HomeCatalogSettingsSnapshot(
val heroEnabled: Boolean, val heroEnabled: Boolean,
val showCatalogType: Boolean, val showCatalogType: Boolean,
val hideUnreleasedContent: Boolean, val hideUnreleasedContent: Boolean,
val hideCatalogUnderline: Boolean,
val preferences: Map<String, HomeCatalogPreference>, val preferences: Map<String, HomeCatalogPreference>,
) )
@ -84,7 +80,6 @@ private data class StoredHomeCatalogSettingsPayload(
val heroEnabled: Boolean = true, val heroEnabled: Boolean = true,
val showCatalogType: Boolean = true, val showCatalogType: Boolean = true,
val hideUnreleasedContent: Boolean = false, val hideUnreleasedContent: Boolean = false,
val hideCatalogUnderline: Boolean = false,
val items: List<StoredHomeCatalogPreference> = emptyList(), val items: List<StoredHomeCatalogPreference> = emptyList(),
) )
@ -106,7 +101,6 @@ object HomeCatalogSettingsRepository {
private var heroEnabled = true private var heroEnabled = true
private var showCatalogType = true private var showCatalogType = true
private var hideUnreleasedContent = false private var hideUnreleasedContent = false
private var hideCatalogUnderline = false
fun onProfileChanged() { fun onProfileChanged() {
hasLoaded = false hasLoaded = false
@ -114,7 +108,6 @@ object HomeCatalogSettingsRepository {
heroEnabled = true heroEnabled = true
showCatalogType = true showCatalogType = true
hideUnreleasedContent = false hideUnreleasedContent = false
hideCatalogUnderline = false
definitions = emptyList() definitions = emptyList()
collectionDefinitions = emptyList() collectionDefinitions = emptyList()
_uiState.value = HomeCatalogSettingsUiState() _uiState.value = HomeCatalogSettingsUiState()
@ -128,7 +121,6 @@ object HomeCatalogSettingsRepository {
heroEnabled = true heroEnabled = true
showCatalogType = true showCatalogType = true
hideUnreleasedContent = false hideUnreleasedContent = false
hideCatalogUnderline = false
_uiState.value = HomeCatalogSettingsUiState() _uiState.value = HomeCatalogSettingsUiState()
} }
@ -162,7 +154,6 @@ object HomeCatalogSettingsRepository {
heroEnabled = heroEnabled, heroEnabled = heroEnabled,
showCatalogType = showCatalogType, showCatalogType = showCatalogType,
hideUnreleasedContent = hideUnreleasedContent, hideUnreleasedContent = hideUnreleasedContent,
hideCatalogUnderline = hideCatalogUnderline,
preferences = preferences.mapValues { (_, value) -> preferences = preferences.mapValues { (_, value) ->
HomeCatalogPreference( HomeCatalogPreference(
customTitle = value.customTitle, customTitle = value.customTitle,
@ -202,15 +193,6 @@ object HomeCatalogSettingsRepository {
HomeCatalogSettingsSyncService.triggerPush() HomeCatalogSettingsSyncService.triggerPush()
} }
fun setHideCatalogUnderline(enabled: Boolean) {
ensureLoaded()
if (hideCatalogUnderline == enabled) return
hideCatalogUnderline = enabled
publish()
persist()
HomeCatalogSettingsSyncService.triggerPush()
}
fun setHeroSourceEnabled(key: String, enabled: Boolean) { fun setHeroSourceEnabled(key: String, enabled: Boolean) {
updatePreference(key, pushRemote = false) { preference -> updatePreference(key, pushRemote = false) { preference ->
if (!enabled) { if (!enabled) {
@ -240,7 +222,6 @@ object HomeCatalogSettingsRepository {
heroEnabled = true heroEnabled = true
showCatalogType = true showCatalogType = true
hideUnreleasedContent = false hideUnreleasedContent = false
hideCatalogUnderline = false
preferences.clear() preferences.clear()
normalizePreferences() normalizePreferences()
publish() publish()
@ -290,7 +271,6 @@ object HomeCatalogSettingsRepository {
heroEnabled = parsedPayload.heroEnabled heroEnabled = parsedPayload.heroEnabled
showCatalogType = parsedPayload.showCatalogType showCatalogType = parsedPayload.showCatalogType
hideUnreleasedContent = parsedPayload.hideUnreleasedContent hideUnreleasedContent = parsedPayload.hideUnreleasedContent
hideCatalogUnderline = parsedPayload.hideCatalogUnderline
preferences = parsedPayload.items.associateBy { it.key }.toMutableMap() preferences = parsedPayload.items.associateBy { it.key }.toMutableMap()
publish() publish()
return return
@ -391,7 +371,6 @@ object HomeCatalogSettingsRepository {
heroEnabled = heroEnabled, heroEnabled = heroEnabled,
showCatalogType = showCatalogType, showCatalogType = showCatalogType,
hideUnreleasedContent = hideUnreleasedContent, hideUnreleasedContent = hideUnreleasedContent,
hideCatalogUnderline = hideCatalogUnderline,
items = items, items = items,
) )
} }
@ -403,7 +382,6 @@ object HomeCatalogSettingsRepository {
heroEnabled = heroEnabled, heroEnabled = heroEnabled,
showCatalogType = showCatalogType, showCatalogType = showCatalogType,
hideUnreleasedContent = hideUnreleasedContent, hideUnreleasedContent = hideUnreleasedContent,
hideCatalogUnderline = hideCatalogUnderline,
items = preferences.values.sortedBy { it.order }, items = preferences.values.sortedBy { it.order },
), ),
), ),
@ -500,7 +478,6 @@ object HomeCatalogSettingsRepository {
return SyncHomeCatalogPayload( return SyncHomeCatalogPayload(
showCatalogType = showCatalogType, showCatalogType = showCatalogType,
hideUnreleasedContent = hideUnreleasedContent, hideUnreleasedContent = hideUnreleasedContent,
hideCatalogUnderline = hideCatalogUnderline,
items = items, items = items,
) )
} }
@ -509,7 +486,6 @@ object HomeCatalogSettingsRepository {
ensureLoaded() ensureLoaded()
showCatalogType = payload.showCatalogType showCatalogType = payload.showCatalogType
hideUnreleasedContent = payload.hideUnreleasedContent hideUnreleasedContent = payload.hideUnreleasedContent
hideCatalogUnderline = payload.hideCatalogUnderline
if (payload.items.isNotEmpty()) { if (payload.items.isNotEmpty()) {
val existingHeroState = preferences.mapValues { it.value.heroSourceEnabled } val existingHeroState = preferences.mapValues { it.value.heroSourceEnabled }
val remotePreferences = payload.items.associate { item -> val remotePreferences = payload.items.associate { item ->

View file

@ -42,7 +42,6 @@ data class SyncCatalogItem(
data class SyncHomeCatalogPayload( data class SyncHomeCatalogPayload(
@SerialName("show_catalog_type") val showCatalogType: Boolean = true, @SerialName("show_catalog_type") val showCatalogType: Boolean = true,
@SerialName("hide_unreleased_content") val hideUnreleasedContent: Boolean = false, @SerialName("hide_unreleased_content") val hideUnreleasedContent: Boolean = false,
@SerialName("hide_catalog_underline") val hideCatalogUnderline: Boolean = false,
val items: List<SyncCatalogItem> = emptyList(), val items: List<SyncCatalogItem> = emptyList(),
) )
@ -59,7 +58,6 @@ private data class RemoteHomeCatalogSettings(
val updatedAt: String?, val updatedAt: String?,
val hasShowCatalogType: Boolean, val hasShowCatalogType: Boolean,
val hasHideUnreleasedContent: Boolean, val hasHideUnreleasedContent: Boolean,
val hasHideCatalogUnderline: Boolean,
) )
private data class PullToken( private data class PullToken(
@ -77,7 +75,6 @@ object HomeCatalogSettingsSyncService {
private const val HIDE_UNRELEASED_CONTENT_KEY = "hide_unreleased_content" private const val HIDE_UNRELEASED_CONTENT_KEY = "hide_unreleased_content"
private const val SHOW_CATALOG_TYPE_KEY = "show_catalog_type" private const val SHOW_CATALOG_TYPE_KEY = "show_catalog_type"
private const val HIDE_CATALOG_UNDERLINE_KEY = "hide_catalog_underline"
@Volatile @Volatile
var isSyncingFromRemote: Boolean = false var isSyncingFromRemote: Boolean = false
@ -221,7 +218,6 @@ object HomeCatalogSettingsSyncService {
updatedAt = blob.updatedAt, updatedAt = blob.updatedAt,
hasShowCatalogType = blob.settingsJson.containsKey(SHOW_CATALOG_TYPE_KEY), hasShowCatalogType = blob.settingsJson.containsKey(SHOW_CATALOG_TYPE_KEY),
hasHideUnreleasedContent = blob.settingsJson.containsKey(HIDE_UNRELEASED_CONTENT_KEY), hasHideUnreleasedContent = blob.settingsJson.containsKey(HIDE_UNRELEASED_CONTENT_KEY),
hasHideCatalogUnderline = blob.settingsJson.containsKey(HIDE_CATALOG_UNDERLINE_KEY),
) )
} }
@ -234,9 +230,6 @@ object HomeCatalogSettingsSyncService {
val showCatalogTypeSource = rows val showCatalogTypeSource = rows
.filter { it.hasShowCatalogType } .filter { it.hasShowCatalogType }
.maxByOrNull { it.updatedAt.orEmpty() } .maxByOrNull { it.updatedAt.orEmpty() }
val hideUnderlineSource = rows
.filter { it.hasHideCatalogUnderline }
.maxByOrNull { it.updatedAt.orEmpty() }
return copy( return copy(
payload = payload.copy( payload = payload.copy(
@ -244,8 +237,6 @@ object HomeCatalogSettingsSyncService {
?: payload.showCatalogType, ?: payload.showCatalogType,
hideUnreleasedContent = hideUnreleasedSource?.payload?.hideUnreleasedContent hideUnreleasedContent = hideUnreleasedSource?.payload?.hideUnreleasedContent
?: payload.hideUnreleasedContent, ?: payload.hideUnreleasedContent,
hideCatalogUnderline = hideUnderlineSource?.payload?.hideCatalogUnderline
?: payload.hideCatalogUnderline,
), ),
) )
} }
@ -278,11 +269,6 @@ object HomeCatalogSettingsSyncService {
} else { } else {
localPayload.hideUnreleasedContent localPayload.hideUnreleasedContent
}, },
hideCatalogUnderline = if (settingsJson.containsKey(HIDE_CATALOG_UNDERLINE_KEY)) {
decoded.hideCatalogUnderline
} else {
localPayload.hideCatalogUnderline
},
) )
}.getOrNull() }.getOrNull()

View file

@ -42,6 +42,17 @@ object HomeRepository {
private var collectionHeroRequestKey: String? = null private var collectionHeroRequestKey: String? = null
private var lastPublishedCatalogHeroEmpty: Boolean = true private var lastPublishedCatalogHeroEmpty: Boolean = true
private var lastErrorMessage: String? = null private var lastErrorMessage: String? = null
private var lastHandledAddonRefreshRevision: Long = 0L
fun refreshAfterAddonRefresh(
addons: List<ManagedAddon>,
revision: Long,
): Boolean {
if (revision <= 0L || revision <= lastHandledAddonRefreshRevision) return false
lastHandledAddonRefreshRevision = revision
refresh(addons, force = true)
return true
}
fun refresh(addons: List<ManagedAddon>, force: Boolean = false) { fun refresh(addons: List<ManagedAddon>, force: Boolean = false) {
val activeAddons = addons.enabledAddons() val activeAddons = addons.enabledAddons()

View file

@ -535,11 +535,19 @@ fun HomeScreen(
buildHomeCatalogRefreshSignature(enabledAddons) buildHomeCatalogRefreshSignature(enabledAddons)
} }
LaunchedEffect(catalogRefreshKey) { LaunchedEffect(catalogRefreshKey, addonsUiState.refreshRevision) {
if (catalogRefreshKey.isEmpty()) return@LaunchedEffect if (catalogRefreshKey.isEmpty() && addonsUiState.refreshRevision == 0L) {
return@LaunchedEffect
}
HomeCatalogSettingsRepository.syncCatalogs(enabledAddons) HomeCatalogSettingsRepository.syncCatalogs(enabledAddons)
val forced = HomeRepository.refreshAfterAddonRefresh(
addons = enabledAddons,
revision = addonsUiState.refreshRevision,
)
if (!forced) {
HomeRepository.refresh(enabledAddons) HomeRepository.refresh(enabledAddons)
} }
}
LaunchedEffect(collections, enabledAddons) { LaunchedEffect(collections, enabledAddons) {
HomeCatalogSettingsRepository.syncCollections(collections) HomeCatalogSettingsRepository.syncCollections(collections)
@ -861,7 +869,6 @@ fun HomeScreen(
hasContinueWatchingRows, hasContinueWatchingRows,
continueWatchingLayout, continueWatchingLayout,
posterCardStyle.widthDp, posterCardStyle.widthDp,
homeSettingsUiState.hideCatalogUnderline,
nativeBottomNavigationOverlayHeight, nativeBottomNavigationOverlayHeight,
) { ) {
if ( if (
@ -873,7 +880,6 @@ fun HomeScreen(
style = continueWatchingPreferences.style, style = continueWatchingPreferences.style,
layout = continueWatchingLayout, layout = continueWatchingLayout,
basePosterWidthDp = posterCardStyle.widthDp, basePosterWidthDp = posterCardStyle.widthDp,
showHeaderAccent = !homeSettingsUiState.hideCatalogUnderline,
) + nativeBottomNavigationOverlayHeight ) + nativeBottomNavigationOverlayHeight
} else { } else {
null null
@ -958,7 +964,6 @@ fun HomeScreen(
items(3) { items(3) {
HomeSkeletonRow( HomeSkeletonRow(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
showHeaderAccent = !homeSettingsUiState.hideCatalogUnderline,
) )
} }
} }

View file

@ -4,15 +4,11 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioShelfSection import com.nuvio.app.core.ui.NuvioShelfSection
import com.nuvio.app.core.ui.NuvioViewAllPillSize import com.nuvio.app.core.ui.NuvioViewAllPillSize
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.HomeCatalogSection import com.nuvio.app.features.home.HomeCatalogSection
import com.nuvio.app.features.home.MetaPreview import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.stableKey import com.nuvio.app.features.home.stableKey
@ -72,10 +68,6 @@ private fun HomeCatalogRowSectionContent(
onPosterLongClick: ((MetaPreview) -> Unit)?, onPosterLongClick: ((MetaPreview) -> Unit)?,
) { ) {
val posterCardStyle = rememberPosterCardStyleUiState() val posterCardStyle = rememberPosterCardStyleUiState()
val homeCatalogSettings by remember {
HomeCatalogSettingsRepository.snapshot()
HomeCatalogSettingsRepository.uiState
}.collectAsStateWithLifecycle()
NuvioShelfSection( NuvioShelfSection(
title = section.title, title = section.title,
@ -83,7 +75,6 @@ private fun HomeCatalogRowSectionContent(
modifier = modifier, modifier = modifier,
headerHorizontalPadding = sectionPadding, headerHorizontalPadding = sectionPadding,
rowContentPadding = PaddingValues(horizontal = sectionPadding), rowContentPadding = PaddingValues(horizontal = sectionPadding),
showHeaderAccent = !homeCatalogSettings.hideCatalogUnderline,
onViewAllClick = onViewAllClick, onViewAllClick = onViewAllClick,
viewAllPillSize = NuvioViewAllPillSize.Compact, viewAllPillSize = NuvioViewAllPillSize.Compact,
key = { item -> item.stableKey() }, key = { item -> item.stableKey() },

View file

@ -15,8 +15,6 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@ -25,7 +23,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioCardDepthSurface import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioShelfSection import com.nuvio.app.core.ui.NuvioShelfSection
import com.nuvio.app.core.ui.PosterLandscapeAspectRatio import com.nuvio.app.core.ui.PosterLandscapeAspectRatio
@ -35,7 +32,6 @@ import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.features.collection.Collection import com.nuvio.app.features.collection.Collection
import com.nuvio.app.features.collection.CollectionFolder import com.nuvio.app.features.collection.CollectionFolder
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.PosterShape import com.nuvio.app.features.home.PosterShape
@Composable @Composable
@ -77,18 +73,12 @@ private fun HomeCollectionRowSectionContent(
animateGifs: Boolean, animateGifs: Boolean,
onFolderClick: ((collectionId: String, folderId: String) -> Unit)?, onFolderClick: ((collectionId: String, folderId: String) -> Unit)?,
) { ) {
val homeCatalogSettings by remember {
HomeCatalogSettingsRepository.snapshot()
HomeCatalogSettingsRepository.uiState
}.collectAsStateWithLifecycle()
NuvioShelfSection( NuvioShelfSection(
title = collection.title, title = collection.title,
entries = collection.folders, entries = collection.folders,
modifier = modifier, modifier = modifier,
headerHorizontalPadding = sectionPadding, headerHorizontalPadding = sectionPadding,
rowContentPadding = PaddingValues(horizontal = sectionPadding), rowContentPadding = PaddingValues(horizontal = sectionPadding),
showHeaderAccent = !homeCatalogSettings.hideCatalogUnderline,
key = { folder -> "collection_${collection.id}_folder_${folder.id}" }, key = { folder -> "collection_${collection.id}_folder_${folder.id}" },
) { folder -> ) { folder ->
CollectionFolderCard( CollectionFolderCard(

View file

@ -47,7 +47,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.DisintegratingContainer import com.nuvio.app.core.ui.DisintegratingContainer
import com.nuvio.app.core.ui.NuvioCardDepthSurface import com.nuvio.app.core.ui.NuvioCardDepthSurface
@ -62,7 +61,6 @@ import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.features.cloud.CloudLibraryContentType import com.nuvio.app.features.cloud.CloudLibraryContentType
import com.nuvio.app.features.cloud.cloudLibraryDisplayArtworkUrl import com.nuvio.app.features.cloud.cloudLibraryDisplayArtworkUrl
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingItem import com.nuvio.app.features.watchprogress.ContinueWatchingItem
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
import com.nuvio.app.features.watchprogress.CurrentDateProvider import com.nuvio.app.features.watchprogress.CurrentDateProvider
@ -87,13 +85,8 @@ internal fun continueWatchingSectionHeightEstimate(
style: ContinueWatchingSectionStyle, style: ContinueWatchingSectionStyle,
layout: ContinueWatchingLayout, layout: ContinueWatchingLayout,
basePosterWidthDp: Int, basePosterWidthDp: Int,
showHeaderAccent: Boolean,
): Dp { ): Dp {
val headerHeight = NuvioTokens.Space.s40 + if (showHeaderAccent) { val headerHeight = NuvioTokens.Space.s40
NuvioTokens.Space.s6 + NuvioTokens.Space.s4
} else {
0.dp
}
val headerToRowGap = NuvioTokens.Space.s8 + NuvioTokens.Space.s2 val headerToRowGap = NuvioTokens.Space.s8 + NuvioTokens.Space.s2
val rowHeight = when (style) { val rowHeight = when (style) {
ContinueWatchingSectionStyle.Card -> continueWatchingLandscapeCardHeight(basePosterWidthDp) ContinueWatchingSectionStyle.Card -> continueWatchingLandscapeCardHeight(basePosterWidthDp)
@ -107,7 +100,6 @@ internal fun continueWatchingHeroViewportReserveHeight(
style: ContinueWatchingSectionStyle, style: ContinueWatchingSectionStyle,
layout: ContinueWatchingLayout, layout: ContinueWatchingLayout,
basePosterWidthDp: Int, basePosterWidthDp: Int,
showHeaderAccent: Boolean,
): Dp { ): Dp {
val bottomNavigationClearance = when (style) { val bottomNavigationClearance = when (style) {
ContinueWatchingSectionStyle.Card, ContinueWatchingSectionStyle.Card,
@ -118,7 +110,6 @@ internal fun continueWatchingHeroViewportReserveHeight(
style = style, style = style,
layout = layout, layout = layout,
basePosterWidthDp = basePosterWidthDp, basePosterWidthDp = basePosterWidthDp,
showHeaderAccent = showHeaderAccent,
) + bottomNavigationClearance ) + bottomNavigationClearance
} }
@ -291,11 +282,6 @@ private fun HomeContinueWatchingSectionContent(
onItemClick: ((ContinueWatchingItem) -> Unit)?, onItemClick: ((ContinueWatchingItem) -> Unit)?,
onItemLongPress: ((ContinueWatchingItem) -> Unit)?, onItemLongPress: ((ContinueWatchingItem) -> Unit)?,
) { ) {
val homeCatalogSettings by remember {
HomeCatalogSettingsRepository.snapshot()
HomeCatalogSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val disintegration = remember { ContinueWatchingDisintegrationHolder() } val disintegration = remember { ContinueWatchingDisintegrationHolder() }
val displayEntries = disintegration.sync(items) val displayEntries = disintegration.sync(items)
@ -306,7 +292,6 @@ private fun HomeContinueWatchingSectionContent(
headerHorizontalPadding = sectionPadding, headerHorizontalPadding = sectionPadding,
rowContentPadding = PaddingValues(horizontal = sectionPadding), rowContentPadding = PaddingValues(horizontal = sectionPadding),
itemSpacing = layout.itemGap, itemSpacing = layout.itemGap,
showHeaderAccent = !homeCatalogSettings.hideCatalogUnderline,
key = { entry -> entry.videoId }, key = { entry -> entry.videoId },
animatePlacement = true, animatePlacement = true,
state = listState, state = listState,

View file

@ -183,7 +183,6 @@ fun HomeSkeletonHero(
@Composable @Composable
fun HomeSkeletonRow( fun HomeSkeletonRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showHeaderAccent: Boolean = true,
) { ) {
val brush = rememberHomeSkeletonBrush() val brush = rememberHomeSkeletonBrush()
val posterCardStyle = rememberPosterCardStyleUiState() val posterCardStyle = rememberPosterCardStyleUiState()
@ -210,17 +209,6 @@ fun HomeSkeletonRow(
.clip(RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp))
.background(brush), .background(brush),
) )
if (showHeaderAccent) {
// Accent bar
Box(
modifier = Modifier
.width(60.dp)
.height(4.dp)
.clip(RoundedCornerShape(999.dp))
.background(brush),
)
Spacer(modifier = Modifier.height(2.dp))
}
// Poster row // Poster row
Row( Row(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),

View file

@ -81,7 +81,6 @@ import com.nuvio.app.features.cloud.CloudLibraryItemType
import com.nuvio.app.features.cloud.CloudLibraryRepository import com.nuvio.app.features.cloud.CloudLibraryRepository
import com.nuvio.app.features.cloud.CloudLibraryUiState import com.nuvio.app.features.cloud.CloudLibraryUiState
import com.nuvio.app.features.debrid.DebridSettingsRepository import com.nuvio.app.features.debrid.DebridSettingsRepository
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.components.HomeEmptyStateCard import com.nuvio.app.features.home.components.HomeEmptyStateCard
import com.nuvio.app.features.home.components.HomePosterCard import com.nuvio.app.features.home.components.HomePosterCard
import com.nuvio.app.features.home.components.HomeSkeletonRow import com.nuvio.app.features.home.components.HomeSkeletonRow
@ -123,10 +122,6 @@ fun LibraryScreen(
LibraryDisplaySettingsRepository.ensureLoaded() LibraryDisplaySettingsRepository.ensureLoaded()
LibraryDisplaySettingsRepository.uiState LibraryDisplaySettingsRepository.uiState
}.collectAsStateWithLifecycle() }.collectAsStateWithLifecycle()
val homeCatalogSettingsUiState by remember {
HomeCatalogSettingsRepository.snapshot()
HomeCatalogSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle() val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
var observedOfflineState by remember { mutableStateOf(false) } var observedOfflineState by remember { mutableStateOf(false) }
var sourceModeName by rememberSaveable { mutableStateOf(LibraryViewMode.Saved.name) } var sourceModeName by rememberSaveable { mutableStateOf(LibraryViewMode.Saved.name) }
@ -344,7 +339,6 @@ fun LibraryScreen(
items(3) { items(3) {
HomeSkeletonRow( HomeSkeletonRow(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
) )
} }
} }
@ -424,7 +418,6 @@ fun LibraryScreen(
LibraryLayoutMode.HORIZONTAL -> librarySections( LibraryLayoutMode.HORIZONTAL -> librarySections(
displaySections = librarySectionsDisplay, displaySections = librarySectionsDisplay,
watchedKeys = watchedUiState.watchedKeys, watchedKeys = watchedUiState.watchedKeys,
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
sortOption = effectiveSortOption, sortOption = effectiveSortOption,
onPosterClick = onPosterClick, onPosterClick = onPosterClick,
onSectionViewAllClick = onSectionViewAllClick, onSectionViewAllClick = onSectionViewAllClick,
@ -1175,7 +1168,6 @@ private enum class LibraryViewMode {
private fun LazyListScope.librarySections( private fun LazyListScope.librarySections(
displaySections: List<LibraryDisplaySection>, displaySections: List<LibraryDisplaySection>,
watchedKeys: Set<String>, watchedKeys: Set<String>,
showHeaderAccent: Boolean,
sortOption: LibrarySortOption, sortOption: LibrarySortOption,
onPosterClick: ((LibraryItem) -> Unit)?, onPosterClick: ((LibraryItem) -> Unit)?,
onSectionViewAllClick: ((LibrarySection, LibrarySortOption) -> Unit)?, onSectionViewAllClick: ((LibrarySection, LibrarySortOption) -> Unit)?,
@ -1192,7 +1184,6 @@ private fun LazyListScope.librarySections(
modifier = libraryContentTransitionModifier(), modifier = libraryContentTransitionModifier(),
headerHorizontalPadding = 16.dp, headerHorizontalPadding = 16.dp,
rowContentPadding = PaddingValues(horizontal = 16.dp), rowContentPadding = PaddingValues(horizontal = 16.dp),
showHeaderAccent = showHeaderAccent,
onViewAllClick = section.source onViewAllClick = section.source
?.takeIf { it.items.size > LIBRARY_SECTION_PREVIEW_LIMIT } ?.takeIf { it.items.size > LIBRARY_SECTION_PREVIEW_LIMIT }
?.let { source -> onSectionViewAllClick?.let { { it(source, sortOption) } } }, ?.let { source -> onSectionViewAllClick?.let { { it(source, sortOption) } } },

View file

@ -318,7 +318,6 @@ fun SearchScreen(
items(2) { items(2) {
HomeSkeletonRow( HomeSkeletonRow(
modifier = Modifier.padding(horizontal = homeSectionPadding), modifier = Modifier.padding(horizontal = homeSectionPadding),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
) )
} }
} }
@ -327,7 +326,6 @@ fun SearchScreen(
items(2) { items(2) {
HomeSkeletonRow( HomeSkeletonRow(
modifier = Modifier.padding(horizontal = homeSectionPadding), modifier = Modifier.padding(horizontal = homeSectionPadding),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
) )
} }
} }
@ -371,7 +369,6 @@ fun SearchScreen(
item(key = "search_loading_more") { item(key = "search_loading_more") {
HomeSkeletonRow( HomeSkeletonRow(
modifier = Modifier.padding(horizontal = homeSectionPadding), modifier = Modifier.padding(horizontal = homeSectionPadding),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
) )
} }
} }

View file

@ -44,8 +44,6 @@ import nuvio.composeapp.generated.resources.layout_catalog_type
import nuvio.composeapp.generated.resources.layout_catalog_type_sub import nuvio.composeapp.generated.resources.layout_catalog_type_sub
import nuvio.composeapp.generated.resources.settings_homescreen_empty_message import nuvio.composeapp.generated.resources.settings_homescreen_empty_message
import nuvio.composeapp.generated.resources.settings_homescreen_empty_title import nuvio.composeapp.generated.resources.settings_homescreen_empty_title
import nuvio.composeapp.generated.resources.settings_homescreen_hide_catalog_underline
import nuvio.composeapp.generated.resources.settings_homescreen_hide_catalog_underline_description
import nuvio.composeapp.generated.resources.settings_homescreen_keep_home_focused import nuvio.composeapp.generated.resources.settings_homescreen_keep_home_focused
import nuvio.composeapp.generated.resources.settings_homescreen_limit_reached import nuvio.composeapp.generated.resources.settings_homescreen_limit_reached
import nuvio.composeapp.generated.resources.settings_homescreen_no_sources_selected import nuvio.composeapp.generated.resources.settings_homescreen_no_sources_selected
@ -70,7 +68,6 @@ internal fun LazyListScope.homescreenSettingsContent(
heroEnabled: Boolean, heroEnabled: Boolean,
showCatalogType: Boolean, showCatalogType: Boolean,
hideUnreleasedContent: Boolean, hideUnreleasedContent: Boolean,
hideCatalogUnderline: Boolean,
items: List<HomeCatalogSettingsItem>, items: List<HomeCatalogSettingsItem>,
) { ) {
val selectedHeroSourceCount = items.count { it.heroSourceEnabled } val selectedHeroSourceCount = items.count { it.heroSourceEnabled }
@ -112,14 +109,6 @@ internal fun LazyListScope.homescreenSettingsContent(
isTablet = isTablet, isTablet = isTablet,
onCheckedChange = HomeCatalogSettingsRepository::setHideUnreleasedContent, onCheckedChange = HomeCatalogSettingsRepository::setHideUnreleasedContent,
) )
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = stringResource(Res.string.settings_homescreen_hide_catalog_underline),
description = stringResource(Res.string.settings_homescreen_hide_catalog_underline_description),
checked = hideCatalogUnderline,
isTablet = isTablet,
onCheckedChange = HomeCatalogSettingsRepository::setHideCatalogUnderline,
)
} }
} }
} }

View file

@ -81,7 +81,6 @@ fun HomescreenSettingsScreen(
heroEnabled = homescreenSettingsUiState.heroEnabled, heroEnabled = homescreenSettingsUiState.heroEnabled,
showCatalogType = homescreenSettingsUiState.showCatalogType, showCatalogType = homescreenSettingsUiState.showCatalogType,
hideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent, hideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
hideCatalogUnderline = homescreenSettingsUiState.hideCatalogUnderline,
items = homescreenSettingsUiState.items, items = homescreenSettingsUiState.items,
) )
} }

View file

@ -405,7 +405,6 @@ fun SettingsScreen(
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled, homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
homescreenShowCatalogType = homescreenSettingsUiState.showCatalogType, homescreenShowCatalogType = homescreenSettingsUiState.showCatalogType,
homescreenHideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent, homescreenHideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
homescreenHideCatalogUnderline = homescreenSettingsUiState.hideCatalogUnderline,
homescreenItems = homescreenSettingsUiState.items, homescreenItems = homescreenSettingsUiState.items,
metaScreenSettingsUiState = metaScreenSettingsUiState, metaScreenSettingsUiState = metaScreenSettingsUiState,
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState, continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
@ -466,7 +465,6 @@ fun SettingsScreen(
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled, homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
homescreenShowCatalogType = homescreenSettingsUiState.showCatalogType, homescreenShowCatalogType = homescreenSettingsUiState.showCatalogType,
homescreenHideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent, homescreenHideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
homescreenHideCatalogUnderline = homescreenSettingsUiState.hideCatalogUnderline,
homescreenItems = homescreenSettingsUiState.items, homescreenItems = homescreenSettingsUiState.items,
metaScreenSettingsUiState = metaScreenSettingsUiState, metaScreenSettingsUiState = metaScreenSettingsUiState,
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState, continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
@ -537,7 +535,6 @@ private fun MobileSettingsScreen(
homescreenHeroEnabled: Boolean, homescreenHeroEnabled: Boolean,
homescreenShowCatalogType: Boolean, homescreenShowCatalogType: Boolean,
homescreenHideUnreleasedContent: Boolean, homescreenHideUnreleasedContent: Boolean,
homescreenHideCatalogUnderline: Boolean,
homescreenItems: List<HomeCatalogSettingsItem>, homescreenItems: List<HomeCatalogSettingsItem>,
metaScreenSettingsUiState: MetaScreenSettingsUiState, metaScreenSettingsUiState: MetaScreenSettingsUiState,
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState, continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
@ -768,7 +765,6 @@ private fun MobileSettingsScreen(
heroEnabled = homescreenHeroEnabled, heroEnabled = homescreenHeroEnabled,
showCatalogType = homescreenShowCatalogType, showCatalogType = homescreenShowCatalogType,
hideUnreleasedContent = homescreenHideUnreleasedContent, hideUnreleasedContent = homescreenHideUnreleasedContent,
hideCatalogUnderline = homescreenHideCatalogUnderline,
items = homescreenItems, items = homescreenItems,
) )
SettingsPage.MetaScreen -> metaScreenSettingsContent( SettingsPage.MetaScreen -> metaScreenSettingsContent(
@ -895,7 +891,6 @@ private fun TabletSettingsScreen(
homescreenHeroEnabled: Boolean, homescreenHeroEnabled: Boolean,
homescreenShowCatalogType: Boolean, homescreenShowCatalogType: Boolean,
homescreenHideUnreleasedContent: Boolean, homescreenHideUnreleasedContent: Boolean,
homescreenHideCatalogUnderline: Boolean,
homescreenItems: List<HomeCatalogSettingsItem>, homescreenItems: List<HomeCatalogSettingsItem>,
metaScreenSettingsUiState: MetaScreenSettingsUiState, metaScreenSettingsUiState: MetaScreenSettingsUiState,
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState, continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
@ -1182,7 +1177,6 @@ private fun TabletSettingsScreen(
heroEnabled = homescreenHeroEnabled, heroEnabled = homescreenHeroEnabled,
showCatalogType = homescreenShowCatalogType, showCatalogType = homescreenShowCatalogType,
hideUnreleasedContent = homescreenHideUnreleasedContent, hideUnreleasedContent = homescreenHideUnreleasedContent,
hideCatalogUnderline = homescreenHideCatalogUnderline,
items = homescreenItems, items = homescreenItems,
) )
SettingsPage.MetaScreen -> metaScreenSettingsContent( SettingsPage.MetaScreen -> metaScreenSettingsContent(

View file

@ -732,7 +732,6 @@ internal fun settingsSearchEntries(
PlaybackSearchRow("home-hero", stringResource(Res.string.settings_homescreen_show_hero), stringResource(Res.string.settings_homescreen_show_hero_description)), PlaybackSearchRow("home-hero", stringResource(Res.string.settings_homescreen_show_hero), stringResource(Res.string.settings_homescreen_show_hero_description)),
PlaybackSearchRow("home-catalog-type", stringResource(Res.string.layout_catalog_type), stringResource(Res.string.layout_catalog_type_sub)), PlaybackSearchRow("home-catalog-type", stringResource(Res.string.layout_catalog_type), stringResource(Res.string.layout_catalog_type_sub)),
PlaybackSearchRow("home-hide-unreleased", stringResource(Res.string.layout_hide_unreleased), stringResource(Res.string.layout_hide_unreleased_sub)), PlaybackSearchRow("home-hide-unreleased", stringResource(Res.string.layout_hide_unreleased), stringResource(Res.string.layout_hide_unreleased_sub)),
PlaybackSearchRow("home-hide-catalog-underline", stringResource(Res.string.settings_homescreen_hide_catalog_underline), stringResource(Res.string.settings_homescreen_hide_catalog_underline_description)),
PlaybackSearchRow("home-hero-sources", stringResource(Res.string.settings_homescreen_section_hero_sources)), PlaybackSearchRow("home-hero-sources", stringResource(Res.string.settings_homescreen_section_hero_sources)),
PlaybackSearchRow("home-catalogs", stringResource(Res.string.settings_homescreen_section_catalogs)), PlaybackSearchRow("home-catalogs", stringResource(Res.string.settings_homescreen_section_catalogs)),
).forEach { row -> ).forEach { row ->

View file

@ -0,0 +1,197 @@
package com.nuvio.app.features.addons
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class AddonRefreshRulesTest {
@Test
fun `remote snapshot replaces order names and enabled states atomically`() {
val preservedManifest = refreshManifest("existing")
val existing = listOf(
ManagedAddon(
manifestUrl = "https://one.example/manifest.json",
manifest = preservedManifest,
userSetName = "Old name",
enabled = false,
),
)
val merged = mergeRemoteAddonSnapshot(
existingAddons = existing,
remoteAddons = listOf(
RemoteAddonValue(
manifestUrl = "https://two.example/manifest.json",
userSetName = "Second",
enabled = false,
),
RemoteAddonValue(
manifestUrl = "https://one.example/manifest.json",
userSetName = "Renamed",
enabled = true,
),
),
forceManifestRefresh = false,
)
assertEquals(
listOf(
"https://two.example/manifest.json",
"https://one.example/manifest.json",
),
merged.map(ManagedAddon::manifestUrl),
)
assertEquals("Second", merged[0].userSetName)
assertFalse(merged[0].enabled)
assertEquals("Renamed", merged[1].userSetName)
assertTrue(merged[1].enabled)
assertSame(preservedManifest, merged[1].manifest)
}
@Test
fun `forced snapshot refreshes enabled manifests only`() {
val merged = mergeRemoteAddonSnapshot(
existingAddons = listOf(
ManagedAddon(
manifestUrl = "https://one.example/manifest.json",
manifest = refreshManifest("one"),
errorMessage = "old error",
),
),
remoteAddons = listOf(
RemoteAddonValue(
manifestUrl = "https://one.example/manifest.json",
userSetName = null,
enabled = true,
),
RemoteAddonValue(
manifestUrl = "https://two.example/manifest.json",
userSetName = null,
enabled = false,
),
),
forceManifestRefresh = true,
)
assertTrue(merged[0].isRefreshing)
assertNull(merged[0].errorMessage)
assertFalse(merged[1].isRefreshing)
}
@Test
fun `remote disable stops an in flight refresh`() {
val merged = mergeRemoteAddonSnapshot(
existingAddons = listOf(
ManagedAddon(
manifestUrl = "https://one.example/manifest.json",
isRefreshing = true,
),
),
remoteAddons = listOf(
RemoteAddonValue(
manifestUrl = "https://one.example/manifest.json",
userSetName = null,
enabled = false,
),
),
forceManifestRefresh = true,
)
assertFalse(merged.single().isRefreshing)
assertFalse(merged.single().enabled)
}
@Test
fun `remote snapshot clears a removed custom name`() {
val merged = mergeRemoteAddonSnapshot(
existingAddons = listOf(
ManagedAddon(
manifestUrl = "https://one.example/manifest.json",
manifest = refreshManifest("one"),
userSetName = "Old custom name",
),
),
remoteAddons = listOf(
RemoteAddonValue(
manifestUrl = "https://one.example/manifest.json",
userSetName = null,
enabled = true,
),
),
forceManifestRefresh = false,
)
assertNull(merged.single().userSetName)
}
@Test
fun `successful empty snapshot clears the profile`() {
val merged = mergeRemoteAddonSnapshot(
existingAddons = listOf(
ManagedAddon(
manifestUrl = "https://one.example/manifest.json",
manifest = refreshManifest("one"),
),
),
remoteAddons = emptyList(),
forceManifestRefresh = true,
)
assertTrue(merged.isEmpty())
}
@Test
fun `remote snapshot is rejected when profile or local state changed`() {
assertTrue(
canApplyRemoteAddonSnapshot(
currentProfileId = 2,
snapshotProfileId = 2,
currentMutationRevision = 7,
expectedMutationRevision = 7,
hasPendingPush = false,
),
)
assertFalse(
canApplyRemoteAddonSnapshot(
currentProfileId = 2,
snapshotProfileId = 1,
currentMutationRevision = 7,
expectedMutationRevision = 7,
hasPendingPush = false,
),
)
assertFalse(
canApplyRemoteAddonSnapshot(
currentProfileId = 2,
snapshotProfileId = 2,
currentMutationRevision = 8,
expectedMutationRevision = 7,
hasPendingPush = false,
),
)
assertFalse(
canApplyRemoteAddonSnapshot(
currentProfileId = 2,
snapshotProfileId = 2,
currentMutationRevision = 7,
expectedMutationRevision = 7,
hasPendingPush = true,
),
)
}
}
private fun refreshManifest(id: String) = AddonManifest(
id = id,
name = id,
description = "",
version = "1.0.0",
resources = listOf(AddonResource(name = "catalog", types = listOf("movie"))),
types = listOf("movie"),
catalogs = listOf(AddonCatalog(type = "movie", id = "popular", name = "Popular")),
transportUrl = "https://$id.example/manifest.json",
)

View file

@ -36,13 +36,11 @@ class HomeHeroSectionTest {
style = ContinueWatchingSectionStyle.Card, style = ContinueWatchingSectionStyle.Card,
layout = continueWatchingLayout, layout = continueWatchingLayout,
basePosterWidthDp = 110, basePosterWidthDp = 110,
showHeaderAccent = true,
) )
val reserveHeight = continueWatchingHeroViewportReserveHeight( val reserveHeight = continueWatchingHeroViewportReserveHeight(
style = ContinueWatchingSectionStyle.Card, style = ContinueWatchingSectionStyle.Card,
layout = continueWatchingLayout, layout = continueWatchingLayout,
basePosterWidthDp = 110, basePosterWidthDp = 110,
showHeaderAccent = true,
) )
val layout = homeHeroLayout( val layout = homeHeroLayout(
maxWidthDp = 390f, maxWidthDp = 390f,

View file

@ -25,6 +25,7 @@ import platform.Foundation.NSUserDefaults
actual object AddonStorage { actual object AddonStorage {
private const val addonUrlsKey = "installed_manifest_urls" private const val addonUrlsKey = "installed_manifest_urls"
private const val addonEnabledStatesKey = "installed_manifest_enabled_states" private const val addonEnabledStatesKey = "installed_manifest_enabled_states"
private const val addonNamesKey = "installed_manifest_names"
actual fun loadInstalledAddonUrls(profileId: Int): List<String> = actual fun loadInstalledAddonUrls(profileId: Int): List<String> =
NSUserDefaults.standardUserDefaults NSUserDefaults.standardUserDefaults
@ -59,6 +60,24 @@ actual object AddonStorage {
forKey = "${addonEnabledStatesKey}_$profileId", forKey = "${addonEnabledStatesKey}_$profileId",
) )
} }
actual fun loadAddonNames(profileId: Int): Map<String, String> =
NSUserDefaults.standardUserDefaults
.stringForKey("${addonNamesKey}_$profileId")
.orEmpty()
.lineSequence()
.mapNotNull(::parseAddonNameLine)
.toMap()
actual fun saveAddonNames(profileId: Int, names: Map<String, String>) {
val payload = names.entries.joinToString(separator = "\n") { (url, name) ->
"$url\t$name"
}
NSUserDefaults.standardUserDefaults.setObject(
payload,
forKey = "${addonNamesKey}_$profileId",
)
}
} }
private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? { private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? {
@ -71,6 +90,12 @@ private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? {
return url to enabled return url to enabled
} }
private fun parseAddonNameLine(line: String): Pair<String, String>? {
val url = line.substringBefore("\t").trim().takeIf { it.isNotEmpty() } ?: return null
val name = line.substringAfter("\t", "").trim().takeIf { it.isNotEmpty() } ?: return null
return url to name
}
private val addonHttpClient = HttpClient(Darwin) { private val addonHttpClient = HttpClient(Darwin) {
install(HttpTimeout) { install(HttpTimeout) {
requestTimeoutMillis = 60_000 requestTimeoutMillis = 60_000