mirror of
https://github.com/NuvioMedia/NuvioMobile.git
synced 2026-07-26 09:02:26 +00:00
Compare commits
4 commits
c241e47b17
...
8242ae314e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8242ae314e | ||
|
|
b60995324b | ||
|
|
7f254a9f2e | ||
|
|
c690be6566 |
46 changed files with 1283 additions and 442 deletions
|
|
@ -568,8 +568,6 @@
|
|||
<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_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_hint">Otvorite katalog samo kada ga trebate preimenovati ili promijeniti njegov redoslijed.</string>
|
||||
<string name="settings_homescreen_visible">Vidljivo</string>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ actual object AddonStorage {
|
|||
private const val preferencesName = "nuvio_addons"
|
||||
private const val addonUrlsKey = "installed_manifest_urls"
|
||||
private const val addonEnabledStatesKey = "installed_manifest_enabled_states"
|
||||
private const val addonNamesKey = "installed_manifest_names"
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
|
|
@ -66,6 +67,24 @@ actual object AddonStorage {
|
|||
?.putString("${addonEnabledStatesKey}_$profileId", payload)
|
||||
?.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>? {
|
||||
|
|
@ -78,6 +97,12 @@ private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? {
|
|||
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()
|
||||
.dns(IPv4FirstDns())
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
|
|
|
|||
|
|
@ -570,8 +570,6 @@
|
|||
<string name="settings_homescreen_show_hero_description">Покажи въртележка в горната част на началото.</string>
|
||||
<string name="layout_hide_unreleased">Скрий невипуснатото съдържание</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_hint">Отворете каталог само когато трябва да го преименувате или пренаредите.</string>
|
||||
<string name="settings_homescreen_visible">Видим</string>
|
||||
|
|
|
|||
|
|
@ -582,8 +582,6 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -1723,8 +1723,6 @@
|
|||
<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_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_licenses_attributions_exoplayer_body">Χρησιμοποιείται για αναπαραγωγή σε εκδόσεις Android.</string>
|
||||
<string name="settings_licenses_attributions_exoplayer_license">Licensed under the Apache License, Έκδοση 2.0.</string>
|
||||
|
|
|
|||
|
|
@ -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="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="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_hint">Abre un catálogo solo cuando necesites cambiarle el nombre o reordenarlo.</string>
|
||||
<string name="settings_homescreen_visible">Visible</string>
|
||||
|
|
|
|||
|
|
@ -592,8 +592,6 @@
|
|||
<string name="settings_homescreen_show_hero_description">Afficher un carrousel Hero en vedette en haut de l’accueil. Choisissez jusqu’à 2 catalogues sources ci-dessous.</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="settings_homescreen_hide_catalog_underline">Masquer le soulignement des catalogues</string>
|
||||
<string name="settings_homescreen_hide_catalog_underline_description">Supprime la ligne d’accent sous les titres de catalogues et de collections dans toute l’app.</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_visible">Visible</string>
|
||||
|
|
|
|||
|
|
@ -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="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="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_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>
|
||||
|
|
|
|||
|
|
@ -547,8 +547,6 @@
|
|||
<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_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_hint">Buka katalog hanya jika perlu mengganti nama atau mengurutkannya.</string>
|
||||
<string name="settings_homescreen_visible">Terlihat</string>
|
||||
|
|
|
|||
|
|
@ -547,8 +547,6 @@
|
|||
<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_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_hint">Buka katalog hanya jika perlu mengganti nama atau mengurutkannya.</string>
|
||||
<string name="settings_homescreen_visible">Terlihat</string>
|
||||
|
|
|
|||
|
|
@ -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="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="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_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>
|
||||
|
|
|
|||
|
|
@ -582,8 +582,6 @@
|
|||
<string name="settings_homescreen_show_hero_description">ホーム上部にヒーローカルーセルを表示します。</string>
|
||||
<string name="layout_hide_unreleased">未公開コンテンツを非表示</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_hint">カタログは名前の変更や並べ替えが必要なときのみ開いてください。</string>
|
||||
<string name="settings_homescreen_visible">表示</string>
|
||||
|
|
|
|||
|
|
@ -531,8 +531,6 @@
|
|||
<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_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_hint">Åpne en katalog kun når du skal gi nytt navn eller endre rekkefølge.</string>
|
||||
<string name="settings_homescreen_visible">Synlig</string>
|
||||
|
|
|
|||
|
|
@ -600,8 +600,6 @@
|
|||
<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_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_hint">Open een catalogus alleen als je deze wilt hernoemen of herordenen.</string>
|
||||
<string name="settings_homescreen_visible">Zichtbaar</string>
|
||||
|
|
|
|||
|
|
@ -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="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="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_hint">Otwórz katalog tylko wtedy, gdy chcesz zmienić jego nazwę lub kolejność.</string>
|
||||
<string name="settings_homescreen_visible">Widoczne</string>
|
||||
|
|
|
|||
|
|
@ -532,8 +532,6 @@
|
|||
<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_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_hint">Abra um catálogo apenas quando precisar renomeá-lo ou reordená-lo.</string>
|
||||
<string name="settings_homescreen_visible">Visível</string>
|
||||
|
|
|
|||
|
|
@ -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="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="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_hint">Abre um catálogo apenas quando precisares de o renomear ou reordenar.</string>
|
||||
<string name="settings_homescreen_visible">Visível</string>
|
||||
|
|
|
|||
|
|
@ -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="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="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_hint">Deschide un catalog doar când trebuie să îl redenumești sau să îl rearanjezi.</string>
|
||||
<string name="settings_homescreen_visible">Vizibil</string>
|
||||
|
|
|
|||
|
|
@ -571,8 +571,6 @@
|
|||
<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_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_hint">Otvorte katalóg iba vtedy, keď ho potrebujete premenovať alebo zmeniť poradie.</string>
|
||||
<string name="settings_homescreen_visible">Viditeľné</string>
|
||||
|
|
|
|||
|
|
@ -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="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="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_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>
|
||||
|
|
|
|||
|
|
@ -579,8 +579,6 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -56,9 +56,24 @@
|
|||
<string name="addons_overview_addons">Addons</string>
|
||||
<string name="addons_overview_catalogs">Catalogs</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_installed">Installed Addons</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_version_format">Version %1$s</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_catalog_type">Show Catalog Type</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_hint">Open a catalog only when you need to rename or reorder it.</string>
|
||||
<string name="settings_homescreen_visible">Visible</string>
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ fun <T> NuvioShelfSection(
|
|||
rowContentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
rowModifier: Modifier = Modifier,
|
||||
itemSpacing: Dp = 10.dp,
|
||||
showHeaderAccent: Boolean = true,
|
||||
onViewAllClick: (() -> Unit)? = null,
|
||||
viewAllPillSize: NuvioViewAllPillSize = NuvioViewAllPillSize.Default,
|
||||
key: ((T) -> Any)? = null,
|
||||
|
|
@ -85,7 +84,6 @@ fun <T> NuvioShelfSection(
|
|||
NuvioShelfSectionHeader(
|
||||
title = title,
|
||||
modifier = Modifier.padding(horizontal = headerHorizontalPadding),
|
||||
showAccent = showHeaderAccent,
|
||||
onViewAllClick = onViewAllClick,
|
||||
viewAllPillSize = viewAllPillSize,
|
||||
)
|
||||
|
|
@ -244,7 +242,6 @@ fun NuvioPosterCard(
|
|||
private fun NuvioShelfSectionHeader(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showAccent: Boolean = true,
|
||||
onViewAllClick: (() -> Unit)? = null,
|
||||
viewAllPillSize: NuvioViewAllPillSize = NuvioViewAllPillSize.Default,
|
||||
) {
|
||||
|
|
@ -278,18 +275,6 @@ private fun NuvioShelfSectionHeader(
|
|||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,8 +67,30 @@ data class ManagedAddon(
|
|||
|
||||
data class AddonsUiState(
|
||||
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(
|
||||
val totalAddons: Int,
|
||||
val activeAddons: Int,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ internal expect object AddonStorage {
|
|||
fun saveInstalledAddonUrls(profileId: Int, urls: List<String>)
|
||||
fun loadAddonEnabledStates(profileId: Int): 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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -105,6 +105,16 @@ internal fun AddonsSettingsPageContent(
|
|||
SectionHeader(stringResource(Res.string.addons_section_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))
|
||||
AddAddonCard(
|
||||
addonUrl = addonUrl,
|
||||
|
|
@ -220,6 +230,54 @@ private fun SectionHeader(text: String) {
|
|||
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
|
||||
private fun OverviewCard(overview: AddonOverview) {
|
||||
NuvioSurfaceCard {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ fun DetailPosterRailSection(
|
|||
horizontal = headerHorizontalPadding + horizontalScrollPadding,
|
||||
),
|
||||
rowModifier = Modifier.nuvioHorizontalScrollBleed(horizontalScrollPadding),
|
||||
showHeaderAccent = false,
|
||||
key = { item -> item.stableKey() },
|
||||
) { item ->
|
||||
HomePosterCard(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ data class HomeCatalogSettingsUiState(
|
|||
val heroEnabled: Boolean = true,
|
||||
val showCatalogType: Boolean = true,
|
||||
val hideUnreleasedContent: Boolean = false,
|
||||
val hideCatalogUnderline: Boolean = false,
|
||||
val items: List<HomeCatalogSettingsItem> = emptyList(),
|
||||
) {
|
||||
val signature: String
|
||||
|
|
@ -45,8 +44,6 @@ data class HomeCatalogSettingsUiState(
|
|||
append('|')
|
||||
append(hideUnreleasedContent)
|
||||
append('|')
|
||||
append(hideCatalogUnderline)
|
||||
append('|')
|
||||
append(
|
||||
items.joinToString(separator = "|") { item ->
|
||||
"${item.key}:${item.order}:${item.enabled}:${item.heroSourceEnabled}:${item.customTitle}"
|
||||
|
|
@ -66,7 +63,6 @@ internal data class HomeCatalogSettingsSnapshot(
|
|||
val heroEnabled: Boolean,
|
||||
val showCatalogType: Boolean,
|
||||
val hideUnreleasedContent: Boolean,
|
||||
val hideCatalogUnderline: Boolean,
|
||||
val preferences: Map<String, HomeCatalogPreference>,
|
||||
)
|
||||
|
||||
|
|
@ -84,7 +80,6 @@ private data class StoredHomeCatalogSettingsPayload(
|
|||
val heroEnabled: Boolean = true,
|
||||
val showCatalogType: Boolean = true,
|
||||
val hideUnreleasedContent: Boolean = false,
|
||||
val hideCatalogUnderline: Boolean = false,
|
||||
val items: List<StoredHomeCatalogPreference> = emptyList(),
|
||||
)
|
||||
|
||||
|
|
@ -106,7 +101,6 @@ object HomeCatalogSettingsRepository {
|
|||
private var heroEnabled = true
|
||||
private var showCatalogType = true
|
||||
private var hideUnreleasedContent = false
|
||||
private var hideCatalogUnderline = false
|
||||
|
||||
fun onProfileChanged() {
|
||||
hasLoaded = false
|
||||
|
|
@ -114,7 +108,6 @@ object HomeCatalogSettingsRepository {
|
|||
heroEnabled = true
|
||||
showCatalogType = true
|
||||
hideUnreleasedContent = false
|
||||
hideCatalogUnderline = false
|
||||
definitions = emptyList()
|
||||
collectionDefinitions = emptyList()
|
||||
_uiState.value = HomeCatalogSettingsUiState()
|
||||
|
|
@ -128,7 +121,6 @@ object HomeCatalogSettingsRepository {
|
|||
heroEnabled = true
|
||||
showCatalogType = true
|
||||
hideUnreleasedContent = false
|
||||
hideCatalogUnderline = false
|
||||
_uiState.value = HomeCatalogSettingsUiState()
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +154,6 @@ object HomeCatalogSettingsRepository {
|
|||
heroEnabled = heroEnabled,
|
||||
showCatalogType = showCatalogType,
|
||||
hideUnreleasedContent = hideUnreleasedContent,
|
||||
hideCatalogUnderline = hideCatalogUnderline,
|
||||
preferences = preferences.mapValues { (_, value) ->
|
||||
HomeCatalogPreference(
|
||||
customTitle = value.customTitle,
|
||||
|
|
@ -202,15 +193,6 @@ object HomeCatalogSettingsRepository {
|
|||
HomeCatalogSettingsSyncService.triggerPush()
|
||||
}
|
||||
|
||||
fun setHideCatalogUnderline(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
if (hideCatalogUnderline == enabled) return
|
||||
hideCatalogUnderline = enabled
|
||||
publish()
|
||||
persist()
|
||||
HomeCatalogSettingsSyncService.triggerPush()
|
||||
}
|
||||
|
||||
fun setHeroSourceEnabled(key: String, enabled: Boolean) {
|
||||
updatePreference(key, pushRemote = false) { preference ->
|
||||
if (!enabled) {
|
||||
|
|
@ -240,7 +222,6 @@ object HomeCatalogSettingsRepository {
|
|||
heroEnabled = true
|
||||
showCatalogType = true
|
||||
hideUnreleasedContent = false
|
||||
hideCatalogUnderline = false
|
||||
preferences.clear()
|
||||
normalizePreferences()
|
||||
publish()
|
||||
|
|
@ -290,7 +271,6 @@ object HomeCatalogSettingsRepository {
|
|||
heroEnabled = parsedPayload.heroEnabled
|
||||
showCatalogType = parsedPayload.showCatalogType
|
||||
hideUnreleasedContent = parsedPayload.hideUnreleasedContent
|
||||
hideCatalogUnderline = parsedPayload.hideCatalogUnderline
|
||||
preferences = parsedPayload.items.associateBy { it.key }.toMutableMap()
|
||||
publish()
|
||||
return
|
||||
|
|
@ -391,7 +371,6 @@ object HomeCatalogSettingsRepository {
|
|||
heroEnabled = heroEnabled,
|
||||
showCatalogType = showCatalogType,
|
||||
hideUnreleasedContent = hideUnreleasedContent,
|
||||
hideCatalogUnderline = hideCatalogUnderline,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
|
|
@ -403,7 +382,6 @@ object HomeCatalogSettingsRepository {
|
|||
heroEnabled = heroEnabled,
|
||||
showCatalogType = showCatalogType,
|
||||
hideUnreleasedContent = hideUnreleasedContent,
|
||||
hideCatalogUnderline = hideCatalogUnderline,
|
||||
items = preferences.values.sortedBy { it.order },
|
||||
),
|
||||
),
|
||||
|
|
@ -500,7 +478,6 @@ object HomeCatalogSettingsRepository {
|
|||
return SyncHomeCatalogPayload(
|
||||
showCatalogType = showCatalogType,
|
||||
hideUnreleasedContent = hideUnreleasedContent,
|
||||
hideCatalogUnderline = hideCatalogUnderline,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
|
|
@ -509,7 +486,6 @@ object HomeCatalogSettingsRepository {
|
|||
ensureLoaded()
|
||||
showCatalogType = payload.showCatalogType
|
||||
hideUnreleasedContent = payload.hideUnreleasedContent
|
||||
hideCatalogUnderline = payload.hideCatalogUnderline
|
||||
if (payload.items.isNotEmpty()) {
|
||||
val existingHeroState = preferences.mapValues { it.value.heroSourceEnabled }
|
||||
val remotePreferences = payload.items.associate { item ->
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ data class SyncCatalogItem(
|
|||
data class SyncHomeCatalogPayload(
|
||||
@SerialName("show_catalog_type") val showCatalogType: Boolean = true,
|
||||
@SerialName("hide_unreleased_content") val hideUnreleasedContent: Boolean = false,
|
||||
@SerialName("hide_catalog_underline") val hideCatalogUnderline: Boolean = false,
|
||||
val items: List<SyncCatalogItem> = emptyList(),
|
||||
)
|
||||
|
||||
|
|
@ -59,7 +58,6 @@ private data class RemoteHomeCatalogSettings(
|
|||
val updatedAt: String?,
|
||||
val hasShowCatalogType: Boolean,
|
||||
val hasHideUnreleasedContent: Boolean,
|
||||
val hasHideCatalogUnderline: Boolean,
|
||||
)
|
||||
|
||||
private data class PullToken(
|
||||
|
|
@ -77,7 +75,6 @@ object HomeCatalogSettingsSyncService {
|
|||
|
||||
private const val HIDE_UNRELEASED_CONTENT_KEY = "hide_unreleased_content"
|
||||
private const val SHOW_CATALOG_TYPE_KEY = "show_catalog_type"
|
||||
private const val HIDE_CATALOG_UNDERLINE_KEY = "hide_catalog_underline"
|
||||
|
||||
@Volatile
|
||||
var isSyncingFromRemote: Boolean = false
|
||||
|
|
@ -221,7 +218,6 @@ object HomeCatalogSettingsSyncService {
|
|||
updatedAt = blob.updatedAt,
|
||||
hasShowCatalogType = blob.settingsJson.containsKey(SHOW_CATALOG_TYPE_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
|
||||
.filter { it.hasShowCatalogType }
|
||||
.maxByOrNull { it.updatedAt.orEmpty() }
|
||||
val hideUnderlineSource = rows
|
||||
.filter { it.hasHideCatalogUnderline }
|
||||
.maxByOrNull { it.updatedAt.orEmpty() }
|
||||
|
||||
return copy(
|
||||
payload = payload.copy(
|
||||
|
|
@ -244,8 +237,6 @@ object HomeCatalogSettingsSyncService {
|
|||
?: payload.showCatalogType,
|
||||
hideUnreleasedContent = hideUnreleasedSource?.payload?.hideUnreleasedContent
|
||||
?: payload.hideUnreleasedContent,
|
||||
hideCatalogUnderline = hideUnderlineSource?.payload?.hideCatalogUnderline
|
||||
?: payload.hideCatalogUnderline,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -278,11 +269,6 @@ object HomeCatalogSettingsSyncService {
|
|||
} else {
|
||||
localPayload.hideUnreleasedContent
|
||||
},
|
||||
hideCatalogUnderline = if (settingsJson.containsKey(HIDE_CATALOG_UNDERLINE_KEY)) {
|
||||
decoded.hideCatalogUnderline
|
||||
} else {
|
||||
localPayload.hideCatalogUnderline
|
||||
},
|
||||
)
|
||||
}.getOrNull()
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,17 @@ object HomeRepository {
|
|||
private var collectionHeroRequestKey: String? = null
|
||||
private var lastPublishedCatalogHeroEmpty: Boolean = true
|
||||
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) {
|
||||
val activeAddons = addons.enabledAddons()
|
||||
|
|
|
|||
|
|
@ -535,10 +535,18 @@ fun HomeScreen(
|
|||
buildHomeCatalogRefreshSignature(enabledAddons)
|
||||
}
|
||||
|
||||
LaunchedEffect(catalogRefreshKey) {
|
||||
if (catalogRefreshKey.isEmpty()) return@LaunchedEffect
|
||||
LaunchedEffect(catalogRefreshKey, addonsUiState.refreshRevision) {
|
||||
if (catalogRefreshKey.isEmpty() && addonsUiState.refreshRevision == 0L) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
HomeCatalogSettingsRepository.syncCatalogs(enabledAddons)
|
||||
HomeRepository.refresh(enabledAddons)
|
||||
val forced = HomeRepository.refreshAfterAddonRefresh(
|
||||
addons = enabledAddons,
|
||||
revision = addonsUiState.refreshRevision,
|
||||
)
|
||||
if (!forced) {
|
||||
HomeRepository.refresh(enabledAddons)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(collections, enabledAddons) {
|
||||
|
|
@ -861,7 +869,6 @@ fun HomeScreen(
|
|||
hasContinueWatchingRows,
|
||||
continueWatchingLayout,
|
||||
posterCardStyle.widthDp,
|
||||
homeSettingsUiState.hideCatalogUnderline,
|
||||
nativeBottomNavigationOverlayHeight,
|
||||
) {
|
||||
if (
|
||||
|
|
@ -873,7 +880,6 @@ fun HomeScreen(
|
|||
style = continueWatchingPreferences.style,
|
||||
layout = continueWatchingLayout,
|
||||
basePosterWidthDp = posterCardStyle.widthDp,
|
||||
showHeaderAccent = !homeSettingsUiState.hideCatalogUnderline,
|
||||
) + nativeBottomNavigationOverlayHeight
|
||||
} else {
|
||||
null
|
||||
|
|
@ -958,7 +964,6 @@ fun HomeScreen(
|
|||
items(3) {
|
||||
HomeSkeletonRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
showHeaderAccent = !homeSettingsUiState.hideCatalogUnderline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,11 @@ import androidx.compose.foundation.layout.BoxWithConstraints
|
|||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.core.ui.NuvioShelfSection
|
||||
import com.nuvio.app.core.ui.NuvioViewAllPillSize
|
||||
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.MetaPreview
|
||||
import com.nuvio.app.features.home.stableKey
|
||||
|
|
@ -72,10 +68,6 @@ private fun HomeCatalogRowSectionContent(
|
|||
onPosterLongClick: ((MetaPreview) -> Unit)?,
|
||||
) {
|
||||
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||
val homeCatalogSettings by remember {
|
||||
HomeCatalogSettingsRepository.snapshot()
|
||||
HomeCatalogSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
||||
NuvioShelfSection(
|
||||
title = section.title,
|
||||
|
|
@ -83,7 +75,6 @@ private fun HomeCatalogRowSectionContent(
|
|||
modifier = modifier,
|
||||
headerHorizontalPadding = sectionPadding,
|
||||
rowContentPadding = PaddingValues(horizontal = sectionPadding),
|
||||
showHeaderAccent = !homeCatalogSettings.hideCatalogUnderline,
|
||||
onViewAllClick = onViewAllClick,
|
||||
viewAllPillSize = NuvioViewAllPillSize.Compact,
|
||||
key = { item -> item.stableKey() },
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import androidx.compose.material3.CardDefaults
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.core.ui.NuvioCardDepthSurface
|
||||
import com.nuvio.app.core.ui.NuvioShelfSection
|
||||
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.features.collection.Collection
|
||||
import com.nuvio.app.features.collection.CollectionFolder
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||
import com.nuvio.app.features.home.PosterShape
|
||||
|
||||
@Composable
|
||||
|
|
@ -77,18 +73,12 @@ private fun HomeCollectionRowSectionContent(
|
|||
animateGifs: Boolean,
|
||||
onFolderClick: ((collectionId: String, folderId: String) -> Unit)?,
|
||||
) {
|
||||
val homeCatalogSettings by remember {
|
||||
HomeCatalogSettingsRepository.snapshot()
|
||||
HomeCatalogSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
||||
NuvioShelfSection(
|
||||
title = collection.title,
|
||||
entries = collection.folders,
|
||||
modifier = modifier,
|
||||
headerHorizontalPadding = sectionPadding,
|
||||
rowContentPadding = PaddingValues(horizontal = sectionPadding),
|
||||
showHeaderAccent = !homeCatalogSettings.hideCatalogUnderline,
|
||||
key = { folder -> "collection_${collection.id}_folder_${folder.id}" },
|
||||
) { folder ->
|
||||
CollectionFolderCard(
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
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
|
||||
|
|
@ -62,7 +61,6 @@ import com.nuvio.app.core.ui.posterCardClickable
|
|||
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||
import com.nuvio.app.features.cloud.CloudLibraryContentType
|
||||
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.ContinueWatchingSectionStyle
|
||||
import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
||||
|
|
@ -87,13 +85,8 @@ internal fun continueWatchingSectionHeightEstimate(
|
|||
style: ContinueWatchingSectionStyle,
|
||||
layout: ContinueWatchingLayout,
|
||||
basePosterWidthDp: Int,
|
||||
showHeaderAccent: Boolean,
|
||||
): Dp {
|
||||
val headerHeight = NuvioTokens.Space.s40 + if (showHeaderAccent) {
|
||||
NuvioTokens.Space.s6 + NuvioTokens.Space.s4
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
val headerHeight = NuvioTokens.Space.s40
|
||||
val headerToRowGap = NuvioTokens.Space.s8 + NuvioTokens.Space.s2
|
||||
val rowHeight = when (style) {
|
||||
ContinueWatchingSectionStyle.Card -> continueWatchingLandscapeCardHeight(basePosterWidthDp)
|
||||
|
|
@ -107,7 +100,6 @@ internal fun continueWatchingHeroViewportReserveHeight(
|
|||
style: ContinueWatchingSectionStyle,
|
||||
layout: ContinueWatchingLayout,
|
||||
basePosterWidthDp: Int,
|
||||
showHeaderAccent: Boolean,
|
||||
): Dp {
|
||||
val bottomNavigationClearance = when (style) {
|
||||
ContinueWatchingSectionStyle.Card,
|
||||
|
|
@ -118,7 +110,6 @@ internal fun continueWatchingHeroViewportReserveHeight(
|
|||
style = style,
|
||||
layout = layout,
|
||||
basePosterWidthDp = basePosterWidthDp,
|
||||
showHeaderAccent = showHeaderAccent,
|
||||
) + bottomNavigationClearance
|
||||
}
|
||||
|
||||
|
|
@ -291,11 +282,6 @@ private fun HomeContinueWatchingSectionContent(
|
|||
onItemClick: ((ContinueWatchingItem) -> Unit)?,
|
||||
onItemLongPress: ((ContinueWatchingItem) -> Unit)?,
|
||||
) {
|
||||
val homeCatalogSettings by remember {
|
||||
HomeCatalogSettingsRepository.snapshot()
|
||||
HomeCatalogSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
||||
val disintegration = remember { ContinueWatchingDisintegrationHolder() }
|
||||
val displayEntries = disintegration.sync(items)
|
||||
|
||||
|
|
@ -306,7 +292,6 @@ private fun HomeContinueWatchingSectionContent(
|
|||
headerHorizontalPadding = sectionPadding,
|
||||
rowContentPadding = PaddingValues(horizontal = sectionPadding),
|
||||
itemSpacing = layout.itemGap,
|
||||
showHeaderAccent = !homeCatalogSettings.hideCatalogUnderline,
|
||||
key = { entry -> entry.videoId },
|
||||
animatePlacement = true,
|
||||
state = listState,
|
||||
|
|
|
|||
|
|
@ -183,7 +183,6 @@ fun HomeSkeletonHero(
|
|||
@Composable
|
||||
fun HomeSkeletonRow(
|
||||
modifier: Modifier = Modifier,
|
||||
showHeaderAccent: Boolean = true,
|
||||
) {
|
||||
val brush = rememberHomeSkeletonBrush()
|
||||
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||
|
|
@ -210,17 +209,6 @@ fun HomeSkeletonRow(
|
|||
.clip(RoundedCornerShape(6.dp))
|
||||
.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
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ import com.nuvio.app.features.cloud.CloudLibraryItemType
|
|||
import com.nuvio.app.features.cloud.CloudLibraryRepository
|
||||
import com.nuvio.app.features.cloud.CloudLibraryUiState
|
||||
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.HomePosterCard
|
||||
import com.nuvio.app.features.home.components.HomeSkeletonRow
|
||||
|
|
@ -123,10 +122,6 @@ fun LibraryScreen(
|
|||
LibraryDisplaySettingsRepository.ensureLoaded()
|
||||
LibraryDisplaySettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val homeCatalogSettingsUiState by remember {
|
||||
HomeCatalogSettingsRepository.snapshot()
|
||||
HomeCatalogSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
||||
var observedOfflineState by remember { mutableStateOf(false) }
|
||||
var sourceModeName by rememberSaveable { mutableStateOf(LibraryViewMode.Saved.name) }
|
||||
|
|
@ -344,7 +339,6 @@ fun LibraryScreen(
|
|||
items(3) {
|
||||
HomeSkeletonRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -424,7 +418,6 @@ fun LibraryScreen(
|
|||
LibraryLayoutMode.HORIZONTAL -> librarySections(
|
||||
displaySections = librarySectionsDisplay,
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
|
||||
sortOption = effectiveSortOption,
|
||||
onPosterClick = onPosterClick,
|
||||
onSectionViewAllClick = onSectionViewAllClick,
|
||||
|
|
@ -1175,7 +1168,6 @@ private enum class LibraryViewMode {
|
|||
private fun LazyListScope.librarySections(
|
||||
displaySections: List<LibraryDisplaySection>,
|
||||
watchedKeys: Set<String>,
|
||||
showHeaderAccent: Boolean,
|
||||
sortOption: LibrarySortOption,
|
||||
onPosterClick: ((LibraryItem) -> Unit)?,
|
||||
onSectionViewAllClick: ((LibrarySection, LibrarySortOption) -> Unit)?,
|
||||
|
|
@ -1192,7 +1184,6 @@ private fun LazyListScope.librarySections(
|
|||
modifier = libraryContentTransitionModifier(),
|
||||
headerHorizontalPadding = 16.dp,
|
||||
rowContentPadding = PaddingValues(horizontal = 16.dp),
|
||||
showHeaderAccent = showHeaderAccent,
|
||||
onViewAllClick = section.source
|
||||
?.takeIf { it.items.size > LIBRARY_SECTION_PREVIEW_LIMIT }
|
||||
?.let { source -> onSectionViewAllClick?.let { { it(source, sortOption) } } },
|
||||
|
|
|
|||
|
|
@ -318,7 +318,6 @@ fun SearchScreen(
|
|||
items(2) {
|
||||
HomeSkeletonRow(
|
||||
modifier = Modifier.padding(horizontal = homeSectionPadding),
|
||||
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -327,7 +326,6 @@ fun SearchScreen(
|
|||
items(2) {
|
||||
HomeSkeletonRow(
|
||||
modifier = Modifier.padding(horizontal = homeSectionPadding),
|
||||
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -371,7 +369,6 @@ fun SearchScreen(
|
|||
item(key = "search_loading_more") {
|
||||
HomeSkeletonRow(
|
||||
modifier = Modifier.padding(horizontal = homeSectionPadding),
|
||||
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.settings_homescreen_empty_message
|
||||
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_limit_reached
|
||||
import nuvio.composeapp.generated.resources.settings_homescreen_no_sources_selected
|
||||
|
|
@ -70,7 +68,6 @@ internal fun LazyListScope.homescreenSettingsContent(
|
|||
heroEnabled: Boolean,
|
||||
showCatalogType: Boolean,
|
||||
hideUnreleasedContent: Boolean,
|
||||
hideCatalogUnderline: Boolean,
|
||||
items: List<HomeCatalogSettingsItem>,
|
||||
) {
|
||||
val selectedHeroSourceCount = items.count { it.heroSourceEnabled }
|
||||
|
|
@ -112,14 +109,6 @@ internal fun LazyListScope.homescreenSettingsContent(
|
|||
isTablet = isTablet,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ fun HomescreenSettingsScreen(
|
|||
heroEnabled = homescreenSettingsUiState.heroEnabled,
|
||||
showCatalogType = homescreenSettingsUiState.showCatalogType,
|
||||
hideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
|
||||
hideCatalogUnderline = homescreenSettingsUiState.hideCatalogUnderline,
|
||||
items = homescreenSettingsUiState.items,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -405,7 +405,6 @@ fun SettingsScreen(
|
|||
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
|
||||
homescreenShowCatalogType = homescreenSettingsUiState.showCatalogType,
|
||||
homescreenHideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
|
||||
homescreenHideCatalogUnderline = homescreenSettingsUiState.hideCatalogUnderline,
|
||||
homescreenItems = homescreenSettingsUiState.items,
|
||||
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
||||
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
||||
|
|
@ -466,7 +465,6 @@ fun SettingsScreen(
|
|||
homescreenHeroEnabled = homescreenSettingsUiState.heroEnabled,
|
||||
homescreenShowCatalogType = homescreenSettingsUiState.showCatalogType,
|
||||
homescreenHideUnreleasedContent = homescreenSettingsUiState.hideUnreleasedContent,
|
||||
homescreenHideCatalogUnderline = homescreenSettingsUiState.hideCatalogUnderline,
|
||||
homescreenItems = homescreenSettingsUiState.items,
|
||||
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
||||
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
||||
|
|
@ -537,7 +535,6 @@ private fun MobileSettingsScreen(
|
|||
homescreenHeroEnabled: Boolean,
|
||||
homescreenShowCatalogType: Boolean,
|
||||
homescreenHideUnreleasedContent: Boolean,
|
||||
homescreenHideCatalogUnderline: Boolean,
|
||||
homescreenItems: List<HomeCatalogSettingsItem>,
|
||||
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
||||
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
||||
|
|
@ -768,7 +765,6 @@ private fun MobileSettingsScreen(
|
|||
heroEnabled = homescreenHeroEnabled,
|
||||
showCatalogType = homescreenShowCatalogType,
|
||||
hideUnreleasedContent = homescreenHideUnreleasedContent,
|
||||
hideCatalogUnderline = homescreenHideCatalogUnderline,
|
||||
items = homescreenItems,
|
||||
)
|
||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||
|
|
@ -895,7 +891,6 @@ private fun TabletSettingsScreen(
|
|||
homescreenHeroEnabled: Boolean,
|
||||
homescreenShowCatalogType: Boolean,
|
||||
homescreenHideUnreleasedContent: Boolean,
|
||||
homescreenHideCatalogUnderline: Boolean,
|
||||
homescreenItems: List<HomeCatalogSettingsItem>,
|
||||
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
||||
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
||||
|
|
@ -1182,7 +1177,6 @@ private fun TabletSettingsScreen(
|
|||
heroEnabled = homescreenHeroEnabled,
|
||||
showCatalogType = homescreenShowCatalogType,
|
||||
hideUnreleasedContent = homescreenHideUnreleasedContent,
|
||||
hideCatalogUnderline = homescreenHideCatalogUnderline,
|
||||
items = homescreenItems,
|
||||
)
|
||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||
|
|
|
|||
|
|
@ -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-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-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-catalogs", stringResource(Res.string.settings_homescreen_section_catalogs)),
|
||||
).forEach { row ->
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -36,13 +36,11 @@ class HomeHeroSectionTest {
|
|||
style = ContinueWatchingSectionStyle.Card,
|
||||
layout = continueWatchingLayout,
|
||||
basePosterWidthDp = 110,
|
||||
showHeaderAccent = true,
|
||||
)
|
||||
val reserveHeight = continueWatchingHeroViewportReserveHeight(
|
||||
style = ContinueWatchingSectionStyle.Card,
|
||||
layout = continueWatchingLayout,
|
||||
basePosterWidthDp = 110,
|
||||
showHeaderAccent = true,
|
||||
)
|
||||
val layout = homeHeroLayout(
|
||||
maxWidthDp = 390f,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import platform.Foundation.NSUserDefaults
|
|||
actual object AddonStorage {
|
||||
private const val addonUrlsKey = "installed_manifest_urls"
|
||||
private const val addonEnabledStatesKey = "installed_manifest_enabled_states"
|
||||
private const val addonNamesKey = "installed_manifest_names"
|
||||
|
||||
actual fun loadInstalledAddonUrls(profileId: Int): List<String> =
|
||||
NSUserDefaults.standardUserDefaults
|
||||
|
|
@ -59,6 +60,24 @@ actual object AddonStorage {
|
|||
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>? {
|
||||
|
|
@ -71,6 +90,12 @@ private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? {
|
|||
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) {
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 60_000
|
||||
|
|
|
|||
Loading…
Reference in a new issue