mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-22 17:52:06 +00:00
Port Reuse Binge Group from the TV version
This commit is contained in:
parent
d29c1e363a
commit
5fe7364d5d
19 changed files with 694 additions and 72 deletions
|
|
@ -44,6 +44,7 @@ import com.nuvio.app.features.updater.AndroidAppUpdaterPlatform
|
|||
import com.nuvio.app.core.ui.PosterCardStyleStorage
|
||||
import com.nuvio.app.features.watched.WatchedStorage
|
||||
import com.nuvio.app.features.streams.StreamLinkCacheStorage
|
||||
import com.nuvio.app.features.streams.BingeGroupCacheStorage
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentStorage
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
|
||||
import com.nuvio.app.features.watchprogress.ResumePromptStorage
|
||||
|
|
@ -87,6 +88,7 @@ class MainActivity : AppCompatActivity() {
|
|||
EpisodeReleaseNotificationsStorage.initialize(applicationContext)
|
||||
WatchProgressStorage.initialize(applicationContext)
|
||||
StreamLinkCacheStorage.initialize(applicationContext)
|
||||
BingeGroupCacheStorage.initialize(applicationContext)
|
||||
PluginStorage.initialize(applicationContext)
|
||||
CollectionMobileSettingsStorage.initialize(applicationContext)
|
||||
CollectionStorage.initialize(applicationContext)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ actual object PlayerSettingsStorage {
|
|||
private const val introSubmitEnabledKey = "intro_submit_enabled"
|
||||
private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled"
|
||||
private const val streamAutoPlayPreferBingeGroupKey = "stream_auto_play_prefer_binge_group"
|
||||
private const val streamAutoPlayReuseBingeGroupKey = "stream_auto_play_reuse_binge_group"
|
||||
private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode"
|
||||
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
|
||||
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||
|
|
@ -101,6 +102,7 @@ actual object PlayerSettingsStorage {
|
|||
animeSkipClientIdKey,
|
||||
streamAutoPlayNextEpisodeEnabledKey,
|
||||
streamAutoPlayPreferBingeGroupKey,
|
||||
streamAutoPlayReuseBingeGroupKey,
|
||||
nextEpisodeThresholdModeKey,
|
||||
nextEpisodeThresholdPercentKey,
|
||||
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||
|
|
@ -609,6 +611,23 @@ actual object PlayerSettingsStorage {
|
|||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadStreamAutoPlayReuseBingeGroup(): Boolean? =
|
||||
preferences?.let { sharedPreferences ->
|
||||
val key = ProfileScopedKey.of(streamAutoPlayReuseBingeGroupKey)
|
||||
if (sharedPreferences.contains(key)) {
|
||||
sharedPreferences.getBoolean(key, true)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
actual fun saveStreamAutoPlayReuseBingeGroup(enabled: Boolean) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putBoolean(ProfileScopedKey.of(streamAutoPlayReuseBingeGroupKey), enabled)
|
||||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadNextEpisodeThresholdMode(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(nextEpisodeThresholdModeKey), null)
|
||||
|
||||
|
|
@ -825,6 +844,7 @@ actual object PlayerSettingsStorage {
|
|||
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
|
||||
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
|
||||
loadStreamAutoPlayReuseBingeGroup()?.let { put(streamAutoPlayReuseBingeGroupKey, encodeSyncBoolean(it)) }
|
||||
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
|
||||
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
|
||||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||
|
|
@ -883,6 +903,7 @@ actual object PlayerSettingsStorage {
|
|||
payload.decodeSyncBoolean(introSubmitEnabledKey)?.let(::saveIntroSubmitEnabled)
|
||||
payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
|
||||
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
|
||||
payload.decodeSyncBoolean(streamAutoPlayReuseBingeGroupKey)?.let(::saveStreamAutoPlayReuseBingeGroup)
|
||||
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
|
||||
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
|
||||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
|
||||
actual object BingeGroupCacheStorage {
|
||||
private const val preferencesName = "nuvio_binge_group_cache"
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
fun initialize(context: Context) {
|
||||
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
actual fun load(hashedKey: String): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(hashedKey), null)
|
||||
|
||||
actual fun save(hashedKey: String, value: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putString(ProfileScopedKey.of(hashedKey), value)
|
||||
?.apply()
|
||||
}
|
||||
|
||||
actual fun remove(hashedKey: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.remove(ProfileScopedKey.of(hashedKey))
|
||||
?.apply()
|
||||
}
|
||||
}
|
||||
|
|
@ -1199,7 +1199,6 @@
|
|||
<string name="library_remove_message">Fjern %1$s fra biblioteket ditt?</string>
|
||||
<string name="library_remove_title">Fjern fra bibliotek?</string>
|
||||
<string name="media_movie">Film</string>
|
||||
<string name="media_series">Serie</string>
|
||||
<string name="notifications_channel_episode_releases_description">Varsler når en ny episode for en lagret serie er ute.</string>
|
||||
<string name="notifications_test_preview_body">Forhåndsvisning av episodeutgivelsesvarsel.</string>
|
||||
<string name="notifications_test_send_failed">Kunne ikke sende testvarsel.</string>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<resources>
|
||||
<string name="about_licenses_attributions_subtitle">Źródła danych, podziękowania i licencje platformy</string>
|
||||
<string name="about_supporters_contributors_subtitle">Osoby wspierające i współtworzące projekt</string>
|
||||
<string name="action_back">Wstecz</string>
|
||||
<string name="action_cancel">Anuluj</string>
|
||||
|
|
@ -17,6 +18,8 @@
|
|||
<string name="action_resume">Wznów</string>
|
||||
<string name="action_retry">Ponów</string>
|
||||
<string name="action_save">Zapisz</string>
|
||||
<string name="action_saving">Zapisywanie…</string>
|
||||
<string name="action_validate">Sprawdź</string>
|
||||
<string name="addon_installing">Instalowanie</string>
|
||||
<string name="addon_title">Dodatki</string>
|
||||
<string name="addons_badge_active">Aktywny</string>
|
||||
|
|
@ -110,29 +113,38 @@
|
|||
<string name="collections_editor_tmdb_production_mode">Produkcja</string>
|
||||
<string name="collections_editor_tmdb_network_mode">Stacja</string>
|
||||
<string name="collections_editor_tmdb_collection_mode">Kolekcja</string>
|
||||
<string name="collections_editor_tmdb_person_mode">Osoba</string>
|
||||
<string name="collections_editor_tmdb_director_mode">Reżyser</string>
|
||||
<string name="collections_editor_tmdb_custom_mode">Niestandardowy</string>
|
||||
<string name="collections_editor_tmdb_help_presets">Wybierz gotowe źródło. Możesz je edytować lub usunąć po dodaniu.</string>
|
||||
<string name="collections_editor_tmdb_help_list">Wklej publiczny URL listy TMDB lub sam numer z URL.</string>
|
||||
<string name="collections_editor_tmdb_help_production">Wyszukaj po nazwie studia lub wklej ID/URL firmy TMDB i dodaj bezpośrednio.</string>
|
||||
<string name="collections_editor_tmdb_help_network">Wprowadź ID stacji. Popularne stacje są dostępne w szablonach i filtrach.</string>
|
||||
<string name="collections_editor_tmdb_help_collection">Wyszukaj nazwę kolekcji filmów lub wklej ID kolekcji z TMDB.</string>
|
||||
<string name="collections_editor_tmdb_help_person">Wprowadź ID osoby TMDB lub URL, aby zbudować wiersz z filmografii aktora.</string>
|
||||
<string name="collections_editor_tmdb_help_director">Wprowadź ID osoby TMDB lub URL, aby zbudować wiersz z filmografii reżysera.</string>
|
||||
<string name="collections_editor_tmdb_help_discover">Zbuduj dynamiczny wiersz TMDB z opcjonalnymi filtrami. Zostaw pola puste, gdy nie potrzebujesz danego filtra.</string>
|
||||
<string name="collections_editor_tmdb_public_list">Publiczna lista TMDB</string>
|
||||
<string name="collections_editor_tmdb_network_id">ID stacji</string>
|
||||
<string name="collections_editor_tmdb_collection_id">ID kolekcji</string>
|
||||
<string name="collections_editor_tmdb_person_id">ID osoby</string>
|
||||
<string name="collections_editor_tmdb_company_search">Nazwa firmy produkcyjnej, ID lub URL</string>
|
||||
<string name="collections_editor_tmdb_id_or_url">ID lub URL TMDB</string>
|
||||
<string name="collections_editor_tmdb_list_placeholder">https://www.themoviedb.org/list/8504994 lub 8504994</string>
|
||||
<string name="collections_editor_tmdb_network_placeholder">213 dla Netflix, 49 dla HBO, 2739 dla Disney+</string>
|
||||
<string name="collections_editor_tmdb_collection_placeholder">10 dla kolekcji Star Wars</string>
|
||||
<string name="collections_editor_tmdb_company_placeholder">Marvel Studios, 420 lub URL firmy</string>
|
||||
<string name="collections_editor_tmdb_person_placeholder">31 dla Toma Hanksa lub URL osoby</string>
|
||||
<string name="collections_editor_tmdb_search_helper">Przykłady: Marvel Studios, 420 lub https://www.themoviedb.org/company/420.</string>
|
||||
<string name="collections_editor_tmdb_collection_helper">Przykład: Star Wars Collection, Harry Potter Collection lub URL kolekcji.</string>
|
||||
<string name="collections_editor_tmdb_network_helper">Przykładowe ID: Netflix 213, HBO 49, Disney+ 2739.</string>
|
||||
<string name="collections_editor_tmdb_list_helper">Przykład: https://www.themoviedb.org/list/8504994 lub 8504994.</string>
|
||||
<string name="collections_editor_tmdb_person_helper">Przykład: https://www.themoviedb.org/person/31-tom-hanks lub 31.</string>
|
||||
<string name="collections_editor_tmdb_display_title">Wyświetlany tytuł</string>
|
||||
<string name="collections_editor_tmdb_title_helper">Wyświetlany jako nazwa wiersza/karty. Jeśli pusty, Nuvio utworzy go ze źródła.</string>
|
||||
<string name="collections_editor_tmdb_title_placeholder">Filmy Marvela, Oryginały Netflix, Pixar</string>
|
||||
<string name="collections_editor_tmdb_person_title_placeholder">Filmy Toma Hanksa, Ulubieni aktorzy</string>
|
||||
<string name="collections_editor_tmdb_director_title_placeholder">Filmy Christophera Nolana, Ulubieni reżyserzy</string>
|
||||
<string name="collections_editor_tmdb_discover_title_placeholder">Najlepsze filmy akcji, Koreańskie dramy, Animacja 2024</string>
|
||||
<string name="collections_editor_tmdb_search_results">Wyniki wyszukiwania</string>
|
||||
<string name="collections_editor_tmdb_collection">Kolekcja TMDB</string>
|
||||
|
|
@ -179,6 +191,27 @@
|
|||
<string name="collections_editor_tmdb_presets">Szablony</string>
|
||||
<string name="collections_editor_tmdb_search">Szukaj</string>
|
||||
<string name="collections_editor_add_source">Dodaj źródło</string>
|
||||
<string name="collections_editor_add_trakt_source">Dodaj listę Trakt</string>
|
||||
<string name="collections_editor_edit_trakt_source">Edytuj listę Trakt</string>
|
||||
<string name="collections_editor_trakt_sources">Listy Trakt</string>
|
||||
<string name="collections_editor_trakt_list">Lista Trakt</string>
|
||||
<string name="collections_editor_trakt_input_placeholder">Szukaj tytułu, URL Trakt lub ID listy</string>
|
||||
<string name="collections_editor_trakt_input_helper">Użyj publicznego URL listy Trakt, numerycznego ID listy lub wyszukaj po nazwie.</string>
|
||||
<string name="collections_editor_trakt_title_placeholder">Weekendowe filmy, Laureaci nagród</string>
|
||||
<string name="collections_editor_trakt_search_results">Wyniki wyszukiwania</string>
|
||||
<string name="collections_editor_trakt_trending">Popularne teraz</string>
|
||||
<string name="collections_editor_trakt_popular">Popularne listy</string>
|
||||
<string name="collections_editor_trakt_direction">Kierunek</string>
|
||||
<string name="collections_editor_trakt_ascending">Rosnąco</string>
|
||||
<string name="collections_editor_trakt_descending">Malejąco</string>
|
||||
<string name="collections_editor_trakt_sort_list_order">Kolejność listy</string>
|
||||
<string name="collections_editor_trakt_sort_recently_added">Ostatnio dodane</string>
|
||||
<string name="collections_editor_trakt_sort_title">Tytuł</string>
|
||||
<string name="collections_editor_trakt_sort_released">Data premiery</string>
|
||||
<string name="collections_editor_trakt_sort_runtime">Czas trwania</string>
|
||||
<string name="collections_editor_trakt_sort_popular">Popularne</string>
|
||||
<string name="collections_editor_trakt_sort_percentage">Procent</string>
|
||||
<string name="collections_editor_trakt_sort_votes">Głosy</string>
|
||||
<string name="collections_editor_tmdb_genre_action">Akcja</string>
|
||||
<string name="collections_editor_tmdb_genre_adventure">Przygodowy</string>
|
||||
<string name="collections_editor_tmdb_genre_animation">Animacja</string>
|
||||
|
|
@ -212,13 +245,29 @@
|
|||
<string name="collections_editor_tmdb_network_disney_plus">Disney+</string>
|
||||
<string name="collections_editor_tmdb_network_prime_video">Prime Video</string>
|
||||
<string name="collections_editor_tmdb_network_hulu">Hulu</string>
|
||||
<string name="collections_editor_tmdb_sort_original">Oryginalna</string>
|
||||
<string name="collections_editor_tmdb_sort_popular">Popularne</string>
|
||||
<string name="collections_editor_tmdb_sort_top_rated">Najwyżej oceniane</string>
|
||||
<string name="collections_editor_tmdb_sort_recent">Ostatnie</string>
|
||||
<string name="collections_editor_tmdb_sort_vote_count">Najczęściej głosowane</string>
|
||||
<string name="collections_editor_tmdb_watch_region">Region dostępności</string>
|
||||
<string name="collections_editor_tmdb_watch_region_helper">Kod kraju ISO 3166-1, w którym tytuł jest dostępny. Przykład: US, GB.</string>
|
||||
<string name="collections_editor_tmdb_quick_watch_regions">Popularne regiony dostępności</string>
|
||||
<string name="collections_editor_tmdb_watch_providers">ID platform streamingowych</string>
|
||||
<string name="collections_editor_tmdb_watch_providers_helper">Użyj ID platform TMDB. Oddziel wiele przecinkami dla AND lub pionowymi kreskami dla OR.</string>
|
||||
<string name="collections_editor_tmdb_watch_providers_placeholder">8|337|350</string>
|
||||
<string name="collections_editor_tmdb_quick_watch_providers">Popularne platformy streamingowe</string>
|
||||
<string name="collections_editor_tmdb_watch_provider_netflix">Netflix</string>
|
||||
<string name="collections_editor_tmdb_watch_provider_prime">Prime Video</string>
|
||||
<string name="collections_editor_tmdb_watch_provider_disney">Disney+</string>
|
||||
<string name="collections_editor_tmdb_watch_provider_apple">Apple TV+</string>
|
||||
<string name="collections_editor_tmdb_watch_provider_hulu">Hulu</string>
|
||||
<string name="collections_editor_tmdb_subtitle_list">Lista TMDB</string>
|
||||
<string name="collections_editor_tmdb_subtitle_movie_collection">Kolekcja filmów TMDB</string>
|
||||
<string name="collections_editor_tmdb_subtitle_production">Produkcja</string>
|
||||
<string name="collections_editor_tmdb_subtitle_network">Stacja</string>
|
||||
<string name="collections_editor_tmdb_subtitle_person">Osoba</string>
|
||||
<string name="collections_editor_tmdb_subtitle_director">Reżyser</string>
|
||||
<string name="collections_editor_tmdb_subtitle_discover">TMDB Discover</string>
|
||||
<string name="collections_empty_subtitle">Utwórz jedną, aby uporządkować katalogi.</string>
|
||||
<string name="collections_empty_title">Brak kolekcji</string>
|
||||
|
|
@ -331,8 +380,10 @@
|
|||
<string name="compose_settings_page_appearance">Wygląd</string>
|
||||
<string name="compose_settings_page_content_discovery">Treści i odkrywanie</string>
|
||||
<string name="compose_settings_page_continue_watching">Kontynuuj oglądanie</string>
|
||||
<string name="compose_settings_page_debrid">Debrid</string>
|
||||
<string name="compose_settings_page_homescreen">Ekran główny</string>
|
||||
<string name="compose_settings_page_integrations">Integracje</string>
|
||||
<string name="compose_settings_page_licenses_attributions">Licencje i atrybucje</string>
|
||||
<string name="compose_settings_page_mdblist_ratings">Oceny MDBList</string>
|
||||
<string name="compose_settings_page_meta_screen">Ekran metadanych</string>
|
||||
<string name="compose_settings_page_notifications">Powiadomienia</string>
|
||||
|
|
@ -358,6 +409,31 @@
|
|||
<string name="compose_settings_root_switch_profile_description">Przełącz na inny profil.</string>
|
||||
<string name="compose_settings_root_switch_profile_title">Przełącz profil</string>
|
||||
<string name="compose_settings_root_trakt_description">Połącz Trakt, synchronizuj listy obserwowanych i zapisuj tytuły bezpośrednio w Trakt.</string>
|
||||
<string name="settings_search_empty">Nie znaleziono ustawień.</string>
|
||||
<string name="settings_search_placeholder">Szukaj ustawień...</string>
|
||||
<string name="settings_search_results_section">WYNIKI</string>
|
||||
<string name="settings_licenses_attributions_section_app">LICENCJA APLIKACJI</string>
|
||||
<string name="settings_licenses_attributions_section_data">DANE I USŁUGI</string>
|
||||
<string name="settings_licenses_attributions_section_playback">LICENCJA ODTWARZANIA</string>
|
||||
<string name="settings_licenses_attributions_nuvio_title">Nuvio Mobile</string>
|
||||
<string name="settings_licenses_attributions_nuvio_body">Kod źródłowy i warunki licencji są dostępne w repozytorium projektu.</string>
|
||||
<string name="settings_licenses_attributions_nuvio_license">Licencjonowany na podstawie GNU General Public License v3.0.</string>
|
||||
<string name="settings_licenses_attributions_tmdb_title">The Movie Database (TMDB)</string>
|
||||
<string name="settings_licenses_attributions_tmdb_body">Nuvio korzysta z API TMDB do metadanych filmów i seriali, grafik, zwiastunów, obsady, szczegółów produkcji, kolekcji i rekomendacji. Ten produkt korzysta z API TMDB, ale nie jest wspierany ani certyfikowany przez TMDB.</string>
|
||||
<string name="settings_licenses_attributions_imdb_title">Niekomercyjne zbiory danych IMDb</string>
|
||||
<string name="settings_licenses_attributions_imdb_body">Nuvio korzysta z niekomercyjnych zbiorów danych IMDb, w tym title.ratings.tsv.gz, do ocen i liczby głosów IMDb. Informacje dzięki uprzejmości IMDb (https://www.imdb.com). Wykorzystywane za zgodą. Dane IMDb służą do użytku osobistego i niekomercyjnego zgodnie z warunkami IMDb.</string>
|
||||
<string name="settings_licenses_attributions_trakt_title">Trakt</string>
|
||||
<string name="settings_licenses_attributions_trakt_body">Nuvio łączy się z Trakt w celu uwierzytelniania konta, historii oglądania, synchronizacji postępu, danych biblioteki, ocen, list i komentarzy. Nuvio nie jest powiązane z Trakt ani przez nie wspierane.</string>
|
||||
<string name="settings_licenses_attributions_mdblist_title">MDBList</string>
|
||||
<string name="settings_licenses_attributions_mdblist_body">Nuvio korzysta z MDBList do ocen i danych zewnętrznych dostawców ocen. Nuvio nie jest powiązane z MDBList ani przez nie wspierane.</string>
|
||||
<string name="settings_licenses_attributions_introdb_title">IntroDB</string>
|
||||
<string name="settings_licenses_attributions_introdb_body">Nuvio korzysta z API IntroDB do dostarczanych przez społeczność znaczników intro, podsumowań, napisów końcowych i podglądów używanych przez kontrolki pomijania. Nuvio nie jest powiązane z IntroDB ani przez nie wspierane.</string>
|
||||
<string name="settings_licenses_attributions_mpvkit_title">MPVKit</string>
|
||||
<string name="settings_licenses_attributions_mpvkit_body">Używany do odtwarzania w wersjach na iOS.</string>
|
||||
<string name="settings_licenses_attributions_mpvkit_license">Kod źródłowy MPVKit jest licencjonowany na podstawie LGPL v3.0. Pakiety MPVKit, w tym biblioteki libmpv i FFmpeg, są również licencjonowane na podstawie LGPL v3.0.</string>
|
||||
<string name="settings_licenses_attributions_exoplayer_title">AndroidX Media3 ExoPlayer 1.8.0</string>
|
||||
<string name="settings_licenses_attributions_exoplayer_body">Używany do odtwarzania w wersjach na Androida.</string>
|
||||
<string name="settings_licenses_attributions_exoplayer_license">Licencjonowany na podstawie Apache License, wersja 2.0.</string>
|
||||
<string name="compose_trakt_list_picker_loading">Ładowanie list Trakt…</string>
|
||||
<string name="compose_trakt_list_picker_subtitle">Wybierz, gdzie zapisać ten tytuł w Trakt</string>
|
||||
<string name="action_donate">Wesprzyj</string>
|
||||
|
|
@ -416,6 +492,8 @@
|
|||
<string name="settings_appearance_app_language">Język aplikacji</string>
|
||||
<string name="settings_appearance_app_language_sheet_title">Wybierz język</string>
|
||||
<string name="settings_appearance_continue_watching_description">Pokaż, ukryj i stylizuj półkę Kontynuuj oglądanie.</string>
|
||||
<string name="settings_appearance_liquid_glass">Liquid Glass</string>
|
||||
<string name="settings_appearance_liquid_glass_description">Użyj natywnego paska kart iPhone na iOS 26 i nowszych. Szybkie przełączanie profili z paska kart jest niedostępne, gdy ta opcja jest włączona.</string>
|
||||
<string name="settings_appearance_poster_customization_description">Dostosuj szerokość i zaokrąglenie rogów kart plakatów.</string>
|
||||
<string name="settings_appearance_section_display">WYŚWIETLANIE</string>
|
||||
<string name="settings_appearance_section_home">EKRAN GŁÓWNY</string>
|
||||
|
|
@ -442,9 +520,14 @@
|
|||
<string name="settings_homescreen_selected_count">%1$d z %2$d wybranych</string>
|
||||
<string name="settings_homescreen_show_hero">Pokaż Hero</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_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>
|
||||
<string name="settings_hide_secret">Ukryj wartość</string>
|
||||
<string name="settings_playback_subtitle">Odtwarzacz, napisy i automatyczne odtwarzanie</string>
|
||||
<string name="settings_poster_card_radius">Zaokrąglenie karty</string>
|
||||
<string name="settings_poster_card_style">STYL KARTY PLAKATU</string>
|
||||
|
|
@ -469,8 +552,19 @@
|
|||
<string name="settings_poster_width_dense">Gęsty</string>
|
||||
<string name="settings_poster_width_large">Duży</string>
|
||||
<string name="settings_poster_width_standard">Standardowy</string>
|
||||
<string name="settings_show_secret">Pokaż wartość</string>
|
||||
<string name="settings_continue_watching_resume_prompt_description">Pokaż okno kontynuowania od miejsca, w którym skończyłeś, po otwarciu aplikacji po wyjściu z odtwarzacza.</string>
|
||||
<string name="settings_continue_watching_resume_prompt_title">Monit o wznowienie przy uruchomieniu</string>
|
||||
<string name="settings_continue_watching_blur_next_up_description">Rozmyj miniatury następnych odcinków w Kontynuuj oglądanie, aby uniknąć spoilerów.</string>
|
||||
<string name="settings_continue_watching_blur_next_up_title">Rozmyj nieobejrzane w Kontynuuj oglądanie</string>
|
||||
<string name="settings_continue_watching_show_unaired_next_up_description">Uwzględnij nadchodzące odcinki w Kontynuuj oglądanie przed ich emisją.</string>
|
||||
<string name="settings_continue_watching_show_unaired_next_up_title">Pokaż niewyemitowane następne odcinki</string>
|
||||
<string name="settings_continue_watching_section_sort_order">KOLEJNOŚĆ SORTOWANIA</string>
|
||||
<string name="settings_continue_watching_sort_mode_title">Kolejność sortowania</string>
|
||||
<string name="settings_continue_watching_sort_mode_default">Domyślna</string>
|
||||
<string name="settings_continue_watching_sort_mode_default_desc">Sortuj wszystkie elementy według czasu</string>
|
||||
<string name="settings_continue_watching_sort_mode_streaming">Styl streamingowy</string>
|
||||
<string name="settings_continue_watching_sort_mode_streaming_desc">Wydane najpierw, nadchodzące na końcu</string>
|
||||
<string name="settings_continue_watching_section_card_style">STYL KARTY</string>
|
||||
<string name="settings_continue_watching_section_on_launch">PRZY URUCHOMIENIU</string>
|
||||
<string name="settings_continue_watching_section_up_next_behavior">ZACHOWANIE NASTĘPNEGO</string>
|
||||
|
|
@ -483,6 +577,8 @@
|
|||
<string name="settings_continue_watching_style_wide_description">Pozioma karta z informacjami</string>
|
||||
<string name="settings_continue_watching_up_next_description">Gdy włączone, Następny zawsze kontynuuje od najdalej obejrzanego odcinka. Gdy wyłączone, kontynuuje od ostatnio obejrzanego. Przydatne przy ponownym oglądaniu wcześniejszych odcinków.</string>
|
||||
<string name="settings_continue_watching_up_next_title">Następny od najdalszego odcinka</string>
|
||||
<string name="settings_continue_watching_use_episode_thumbnails_description">Preferuj miniatury odcinków, gdy są dostępne.</string>
|
||||
<string name="settings_continue_watching_use_episode_thumbnails_title">Preferuj miniatury odcinków w Kontynuuj oglądanie</string>
|
||||
<string name="settings_content_discovery_section_home">EKRAN GŁÓWNY</string>
|
||||
<string name="settings_content_discovery_section_sources">ŹRÓDŁA</string>
|
||||
<string name="settings_content_discovery_addons_description">Instaluj, usuwaj, odświeżaj i sortuj źródła treści.</string>
|
||||
|
|
@ -493,6 +589,34 @@
|
|||
<string name="settings_integrations_section_title">INTEGRACJE</string>
|
||||
<string name="settings_integrations_tmdb_description">Wzbogać strony szczegółów grafikami TMDB, obsadą, metadanymi odcinków i nie tylko.</string>
|
||||
<string name="settings_integrations_mdblist_description">Dodaj oceny IMDb, Rotten Tomatoes, Metacritic i inne zewnętrzne oceny do stron szczegółów.</string>
|
||||
<string name="settings_integrations_debrid_description">Eksperymentalne źródła z kont chmurowych</string>
|
||||
<string name="settings_debrid_section_title">Debrid</string>
|
||||
<string name="settings_debrid_experimental_notice">Obsługa Debrid jest eksperymentalna i może zostać zachowana, zmieniona lub usunięta w przyszłości.</string>
|
||||
<string name="settings_debrid_enable">Włącz źródła</string>
|
||||
<string name="settings_debrid_enable_description">Pokaż odtwarzalne wyniki z połączonych kont.</string>
|
||||
<string name="settings_debrid_add_key_first">Najpierw dodaj klucz API.</string>
|
||||
<string name="settings_debrid_section_providers">Konto</string>
|
||||
<string name="settings_debrid_provider_torbox_description">Połącz swoje konto Torbox.</string>
|
||||
<string name="settings_debrid_dialog_title">Klucz API Torbox</string>
|
||||
<string name="settings_debrid_dialog_subtitle">Wprowadź swój klucz API Torbox.</string>
|
||||
<string name="settings_debrid_dialog_placeholder">Wprowadź klucz API Torbox</string>
|
||||
<string name="settings_debrid_not_set">Nie ustawiono</string>
|
||||
<string name="settings_debrid_section_instant_playback">Natychmiastowe odtwarzanie</string>
|
||||
<string name="settings_debrid_prepare_instant_playback">Przygotuj linki</string>
|
||||
<string name="settings_debrid_prepare_instant_playback_description">Rozwiąż pierwsze źródła przed rozpoczęciem odtwarzania.</string>
|
||||
<string name="settings_debrid_prepare_stream_count">Źródła do przygotowania</string>
|
||||
<string name="settings_debrid_prepare_stream_count_warning">Używaj niższej liczby, gdy to możliwe. Usługi Debrid mogą ograniczać liczbę linków rozwiązywanych w danym okresie. Otwarcie filmu lub odcinka może się wliczać do tych limitów, nawet jeśli nie naciśniesz Odtwórz, ponieważ linki są przygotowywane z wyprzedzeniem.</string>
|
||||
<string name="settings_debrid_prepare_count_one">1 źródło</string>
|
||||
<string name="settings_debrid_prepare_count_many">%1$d źródeł</string>
|
||||
<string name="settings_debrid_section_formatting">Formatowanie</string>
|
||||
<string name="settings_debrid_name_template">Szablon nazwy</string>
|
||||
<string name="settings_debrid_name_template_description">Kontroluje sposób wyświetlania nazw źródeł.</string>
|
||||
<string name="settings_debrid_description_template">Szablon opisu</string>
|
||||
<string name="settings_debrid_description_template_description">Kontroluje metadane wyświetlane pod każdym źródłem.</string>
|
||||
<string name="settings_debrid_formatter_reset_title">Resetuj formatowanie</string>
|
||||
<string name="settings_debrid_formatter_reset_subtitle">Przywróć domyślne formatowanie źródeł.</string>
|
||||
<string name="settings_debrid_key_valid">Klucz API zweryfikowany.</string>
|
||||
<string name="settings_debrid_key_invalid">Nie udało się zweryfikować tego klucza API.</string>
|
||||
<string name="settings_mdb_add_api_key_first">Dodaj klucz API MDBList poniżej przed włączeniem ocen.</string>
|
||||
<string name="settings_mdb_api_key_description">Pobierz klucz z https://mdblist.com/preferences i wklej go tutaj.</string>
|
||||
<string name="settings_mdb_api_key_label">Klucz API</string>
|
||||
|
|
@ -522,6 +646,8 @@
|
|||
<string name="settings_meta_episode_style_list_description">Karty ze szczegółami na pierwszym planie</string>
|
||||
<string name="settings_meta_episodes">Odcinki</string>
|
||||
<string name="settings_meta_episodes_description">Sezony i lista odcinków dla seriali.</string>
|
||||
<string name="settings_meta_blur_unwatched_episodes">Rozmyj nieobejrzane odcinki</string>
|
||||
<string name="settings_meta_blur_unwatched_episodes_description">Rozmyj miniatury odcinków do momentu obejrzenia, aby uniknąć spoilerów.</string>
|
||||
<string name="settings_meta_group_label">Grupa %1$d</string>
|
||||
<string name="settings_meta_more_like_this">Podobne</string>
|
||||
<string name="settings_meta_more_like_this_description">Wiersz rekomendacji.</string>
|
||||
|
|
@ -588,6 +714,10 @@
|
|||
<string name="settings_playback_anime_skip">Anime Skip</string>
|
||||
<string name="settings_playback_anime_skip_client_id">ID klienta AnimeSkip</string>
|
||||
<string name="settings_playback_anime_skip_client_id_description">Wprowadź ID klienta API AnimeSkip. Pobierz je na anime-skip.com.</string>
|
||||
<string name="settings_playback_intro_submit_enabled">Włącz przesyłanie intro</string>
|
||||
<string name="settings_playback_intro_submit_enabled_description">Pokaż przycisk do przesyłania znaczników intro/outro do bazy społeczności.</string>
|
||||
<string name="settings_playback_introdb_api_key">Klucz API IntroDB</string>
|
||||
<string name="settings_playback_introdb_api_key_description">Wprowadź klucz API IntroDB, aby przesyłać znaczniki czasowe. Wymagany do przesyłania.</string>
|
||||
<string name="settings_playback_anime_skip_description">Wyszukuj również w AnimeSkip znaczniki pomijania (wymaga ID klienta).</string>
|
||||
<string name="settings_playback_auto_play_next_episode">Automatyczne odtwarzanie następnego odcinka</string>
|
||||
<string name="settings_playback_auto_play_next_episode_description">Automatycznie znajdź i odtwórz następny odcinek po osiągnięciu progu.</string>
|
||||
|
|
@ -603,6 +733,11 @@
|
|||
<string name="settings_playback_duration_hours">%1$d godzin</string>
|
||||
<string name="settings_playback_enable_libass">Włącz libass</string>
|
||||
<string name="settings_playback_enable_libass_description">Użyj libass do renderowania napisów ASS/SSA zamiast domyślnego renderera.</string>
|
||||
<string name="settings_playback_external_player">Zewnętrzny odtwarzacz</string>
|
||||
<string name="settings_playback_external_player_app">Aplikacja zewnętrznego odtwarzacza</string>
|
||||
<string name="settings_playback_external_player_description_android">Otwórz nowe odtwarzanie w domyślnej aplikacji wideo Androida lub selektorze systemowym.</string>
|
||||
<string name="settings_playback_external_player_description_ios">Otwórz nowe odtwarzanie w wybranym zainstalowanym odtwarzaczu.</string>
|
||||
<string name="settings_playback_external_player_none_available">Brak zainstalowanych obsługiwanych zewnętrznych odtwarzaczy</string>
|
||||
<string name="settings_playback_hold_speed">Prędkość przy przytrzymaniu</string>
|
||||
<string name="settings_playback_hold_to_speed">Przytrzymaj, aby przyspieszyć</string>
|
||||
<string name="settings_playback_hold_to_speed_description">Przytrzymaj dowolne miejsce na powierzchni odtwarzacza, aby tymczasowo zwiększyć prędkość odtwarzania.</string>
|
||||
|
|
@ -621,6 +756,8 @@
|
|||
<string name="settings_playback_option_none">Brak</string>
|
||||
<string name="settings_playback_prefer_binge_group">Preferuj grupę binge</string>
|
||||
<string name="settings_playback_prefer_binge_group_description">Przy automatycznym odtwarzaniu preferuj strumień z tej samej grupy binge co bieżący.</string>
|
||||
<string name="settings_playback_reuse_binge_group">Ponownie użyj grupy binge</string>
|
||||
<string name="settings_playback_reuse_binge_group_description">Zapamiętaj i ponownie użyj ostatniej grupy binge między sesjami (Kontynuuj oglądanie, Szczegóły itp.).</string>
|
||||
<string name="settings_playback_preferred_audio_language">Preferowany język audio</string>
|
||||
<string name="settings_playback_preferred_subtitle_language">Preferowany język napisów</string>
|
||||
<string name="settings_playback_presets">Szablony</string>
|
||||
|
|
@ -744,6 +881,28 @@
|
|||
<string name="settings_trakt_open_login">Otwórz logowanie Trakt</string>
|
||||
<string name="settings_trakt_save_actions_description">Twoje akcje zapisywania mogą teraz celować w listę obserwowanych i osobiste listy Trakt.</string>
|
||||
<string name="settings_trakt_sign_in_description">Zaloguj się w Trakt, aby włączyć zapisywanie na listach i tryb biblioteki Trakt.</string>
|
||||
<string name="trakt_library_source_title">Źródło biblioteki</string>
|
||||
<string name="trakt_library_source_subtitle">Wybierz, której biblioteki używać do zapisywania i przeglądania kolekcji</string>
|
||||
<string name="trakt_library_source_dialog_title">Źródło biblioteki</string>
|
||||
<string name="trakt_library_source_dialog_subtitle">Wybierz, gdzie zapisywać i zarządzać elementami biblioteki</string>
|
||||
<string name="trakt_library_source_trakt">Trakt</string>
|
||||
<string name="trakt_library_source_nuvio">Biblioteka Nuvio</string>
|
||||
<string name="trakt_library_source_trakt_selected">Wybrano bibliotekę Trakt</string>
|
||||
<string name="trakt_library_source_nuvio_selected">Wybrano bibliotekę Nuvio</string>
|
||||
<string name="trakt_watch_progress_title">Postęp oglądania</string>
|
||||
<string name="trakt_watch_progress_subtitle">Wybierz, które źródło postępu obsługuje wznawianie i Kontynuuj oglądanie</string>
|
||||
<string name="trakt_watch_progress_dialog_title">Postęp oglądania</string>
|
||||
<string name="trakt_watch_progress_dialog_subtitle">Wybierz, czy wznawianie i Kontynuuj oglądanie powinno korzystać z Trakt czy Nuvio Sync, podczas gdy scrobblowanie Trakt pozostaje aktywne.</string>
|
||||
<string name="trakt_watch_progress_source_trakt">Trakt</string>
|
||||
<string name="trakt_watch_progress_source_nuvio">Nuvio Sync</string>
|
||||
<string name="trakt_watch_progress_trakt_selected">Źródło postępu ustawione na Trakt</string>
|
||||
<string name="trakt_watch_progress_nuvio_selected">Źródło postępu ustawione na Nuvio Sync</string>
|
||||
<string name="trakt_continue_watching_window">Okno Kontynuuj oglądanie</string>
|
||||
<string name="trakt_continue_watching_subtitle">Historia Trakt uwzględniana w Kontynuuj oglądanie</string>
|
||||
<string name="trakt_cw_window_title">Okno Kontynuuj oglądanie</string>
|
||||
<string name="trakt_cw_window_subtitle">Wybierz, ile aktywności Trakt ma się pojawiać w Kontynuuj oglądanie.</string>
|
||||
<string name="trakt_all_history">Cała historia</string>
|
||||
<string name="trakt_days_format">%1$d dni</string>
|
||||
<string name="source_audience_score">Ocena widzów</string>
|
||||
<string name="source_imdb">IMDb</string>
|
||||
<string name="source_letterboxd">Letterboxd</string>
|
||||
|
|
@ -934,9 +1093,14 @@
|
|||
<string name="pin_locked_try_again">Zablokowane. Spróbuj ponownie za %1$ds</string>
|
||||
<string name="profile_avatar_options_pending">Opcje awatarów pojawią się tutaj po załadowaniu katalogu.</string>
|
||||
<string name="profile_avatar_selected">Awatar: %1$s</string>
|
||||
<string name="profile_avatar_url_invalid">Wprowadź prawidłowy URL obrazu http:// lub https://.</string>
|
||||
<string name="profile_choose_avatar">Wybierz awatar</string>
|
||||
<string name="profile_choose_avatar_below">Wybierz awatar poniżej.</string>
|
||||
<string name="profile_create_profile">Utwórz profil</string>
|
||||
<string name="profile_custom_avatar_selected">Wybrano niestandardowy URL awatara.</string>
|
||||
<string name="profile_custom_avatar_url">Niestandardowy URL awatara</string>
|
||||
<string name="profile_custom_avatar_url_description">Wklej link do obrazu lub zostaw puste, aby użyć wbudowanego katalogu awatarów.</string>
|
||||
<string name="profile_custom_avatar_url_placeholder">https://example.com/avatar.png</string>
|
||||
<string name="profile_delete_confirm_message">Wszystkie dane profilu „%1$s" zostaną trwale usunięte.</string>
|
||||
<string name="profile_delete_title">Usuń profil</string>
|
||||
<string name="profile_edit_add_title">Dodaj profil</string>
|
||||
|
|
@ -968,6 +1132,8 @@
|
|||
<string name="streams_checking_more_addons">Sprawdzanie kolejnych dodatków…</string>
|
||||
<string name="streams_copy_link">Kopiuj link strumienia</string>
|
||||
<string name="streams_download_file">Pobierz plik</string>
|
||||
<string name="streams_open_external_player">Otwórz w zewnętrznym odtwarzaczu</string>
|
||||
<string name="streams_open_internal_player">Otwórz w wewnętrznym odtwarzaczu</string>
|
||||
<string name="streams_empty_load_failed_message">Zainstalowane dodatki strumieni nie zwróciły prawidłowej odpowiedzi.</string>
|
||||
<string name="streams_empty_load_failed_title">Nie można załadować strumieni</string>
|
||||
<string name="streams_empty_no_addons_message">Najpierw zainstaluj dodatek, aby załadować strumienie dla tego tytułu.</string>
|
||||
|
|
@ -987,6 +1153,13 @@
|
|||
<string name="streams_resume_from_percent">Wznów od %1$d%</string>
|
||||
<string name="streams_resume_from_time">Wznów od %1$s</string>
|
||||
<string name="streams_size">ROZMIAR %1$s</string>
|
||||
<string name="streams_torrent_not_supported">Ten typ strumienia nie jest obsługiwany</string>
|
||||
<string name="debrid_missing_api_key">Dodaj klucz API Debrid w Ustawieniach.</string>
|
||||
<string name="debrid_stream_stale">Ten wynik Debrid wygasł. Odświeżanie strumieni.</string>
|
||||
<string name="debrid_resolve_failed">Nie udało się rozwiązać tego strumienia Debrid.</string>
|
||||
<string name="external_player_failed">Nie udało się otworzyć zewnętrznego odtwarzacza</string>
|
||||
<string name="external_player_not_configured">Najpierw wybierz zewnętrzny odtwarzacz w ustawieniach</string>
|
||||
<string name="external_player_unavailable">Brak dostępnego zewnętrznego odtwarzacza</string>
|
||||
<string name="trailer_close">Zamknij zwiastun</string>
|
||||
<string name="trailer_unable_to_play">Nie można odtworzyć zwiastuna</string>
|
||||
<string name="trakt_lists_load_failed">Nie udało się załadować list Trakt</string>
|
||||
|
|
@ -1032,6 +1205,7 @@
|
|||
<string name="downloads_live_failed">Pobieranie nie powiodło się</string>
|
||||
<string name="downloads_live_paused">Wstrzymano %1$s</string>
|
||||
<string name="library_remove_confirm">Usuń</string>
|
||||
<string name="library_remove_from_list_message">Usunąć %1$s z %2$s?</string>
|
||||
<string name="library_remove_message">Usunąć %1$s z biblioteki?</string>
|
||||
<string name="library_remove_title">Usunąć z biblioteki?</string>
|
||||
<string name="media_movie">Film</string>
|
||||
|
|
@ -1075,6 +1249,7 @@
|
|||
<string name="collections_import_error_folder_blank_id">Folder %1$d w „%2$s" ma puste id.</string>
|
||||
<string name="collections_import_error_folder_blank_title">Folder „%1$s" w „%2$s" ma pusty tytuł.</string>
|
||||
<string name="collections_import_error_source_blank_fields">Źródło %1$d w folderze „%2$s" ma puste pola.</string>
|
||||
<string name="collections_import_error_trakt_list_id">Źródło %1$d w folderze '%2$s' nie ma ID listy Trakt.</string>
|
||||
<string name="collections_import_error_invalid_json">Nieprawidłowy JSON: %1$s</string>
|
||||
<string name="collections_folder_addon_not_found">Nie znaleziono dodatku: %1$s</string>
|
||||
<string name="date_month_january">Styczeń</string>
|
||||
|
|
@ -1148,6 +1323,14 @@
|
|||
<string name="notifications_episode_release_body_generic">Nowy odcinek jest już dostępny</string>
|
||||
<string name="notifications_episode_release_body_title">%1$s jest już dostępny</string>
|
||||
<string name="notifications_channel_episode_releases_name">Premiery odcinków</string>
|
||||
<string name="parental_alcohol">Alkohol/Narkotyki</string>
|
||||
<string name="parental_frightening">Przerażające</string>
|
||||
<string name="parental_nudity">Nagość</string>
|
||||
<string name="parental_profanity">Wulgaryzmy</string>
|
||||
<string name="parental_severity_mild">Łagodne</string>
|
||||
<string name="parental_severity_moderate">Umiarkowane</string>
|
||||
<string name="parental_severity_severe">Intensywne</string>
|
||||
<string name="parental_violence">Przemoc</string>
|
||||
<string name="person_role_creator">Twórca</string>
|
||||
<string name="person_role_director">Reżyser</string>
|
||||
<string name="person_role_writer">Scenarzysta</string>
|
||||
|
|
|
|||
|
|
@ -756,6 +756,8 @@
|
|||
<string name="settings_playback_option_none">None</string>
|
||||
<string name="settings_playback_prefer_binge_group">Prefer Binge Group (Next Episode)</string>
|
||||
<string name="settings_playback_prefer_binge_group_description">Try the same source profile first (same addon/quality group) before normal auto-play rules.</string>
|
||||
<string name="settings_playback_reuse_binge_group">Reuse Binge Group</string>
|
||||
<string name="settings_playback_reuse_binge_group_description">Remember and reuse the last binge group across sessions (Continue Watching, Details, etc.).</string>
|
||||
<string name="settings_playback_preferred_audio_language">Preferred Audio Language</string>
|
||||
<string name="settings_playback_preferred_subtitle_language">Preferred Language</string>
|
||||
<string name="settings_playback_presets">Presets</string>
|
||||
|
|
|
|||
|
|
@ -1527,6 +1527,7 @@ private fun MainAppContent(
|
|||
StreamsRepository.reload(
|
||||
type = launch.type,
|
||||
videoId = effectiveVideoId,
|
||||
parentMetaId = launch.parentMetaId,
|
||||
season = launch.seasonNumber,
|
||||
episode = launch.episodeNumber,
|
||||
manualSelection = launch.manualSelection,
|
||||
|
|
@ -1636,6 +1637,7 @@ private fun MainAppContent(
|
|||
StreamsRepository.reload(
|
||||
type = launch.type,
|
||||
videoId = effectiveVideoId,
|
||||
parentMetaId = launch.parentMetaId,
|
||||
season = launch.seasonNumber,
|
||||
episode = launch.episodeNumber,
|
||||
manualSelection = launch.manualSelection,
|
||||
|
|
|
|||
|
|
@ -56,8 +56,10 @@ import com.nuvio.app.features.player.skip.PlayerNextEpisodeRules
|
|||
import com.nuvio.app.features.player.skip.SkipIntroButton
|
||||
import com.nuvio.app.features.player.skip.SkipIntroRepository
|
||||
import com.nuvio.app.features.player.skip.SkipInterval
|
||||
import com.nuvio.app.features.streams.BingeGroupCacheRepository
|
||||
import com.nuvio.app.features.streams.StreamAutoPlayMode
|
||||
import com.nuvio.app.features.streams.StreamAutoPlaySelector
|
||||
import com.nuvio.app.features.streams.StreamAutoPlaySource
|
||||
import com.nuvio.app.features.streams.StreamItem
|
||||
import com.nuvio.app.features.streams.StreamLinkCacheRepository
|
||||
import com.nuvio.app.features.streams.StreamsUiState
|
||||
|
|
@ -73,6 +75,7 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlin.math.abs
|
||||
|
|
@ -86,6 +89,8 @@ private const val PlayerLockedOverlayDurationMs = 2_000L
|
|||
private const val PlayerLeftGestureBoundary = 0.4f
|
||||
private const val PlayerRightGestureBoundary = 0.6f
|
||||
private const val PlayerVerticalGestureSensitivity = 1f
|
||||
/** Hard ceiling for next-episode stream search to prevent hanging forever. */
|
||||
private const val NEXT_EPISODE_HARD_TIMEOUT_MS = 120_000L
|
||||
private val PlayerSliderOverlayGap = 12.dp
|
||||
private val PlayerTimeRowHeight = 36.dp
|
||||
private val PlayerActionRowHeight = 50.dp
|
||||
|
|
@ -323,6 +328,15 @@ fun PlayerScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// Persist binge group per content so subsequent episode plays
|
||||
// (from CW, Details, or next-episode) can reuse the same source group.
|
||||
LaunchedEffect(currentStreamBingeGroup, parentMetaId) {
|
||||
val bg = currentStreamBingeGroup
|
||||
if (bg != null && parentMetaId.isNotBlank()) {
|
||||
BingeGroupCacheRepository.save(parentMetaId, bg)
|
||||
}
|
||||
}
|
||||
|
||||
ManagePlayerPictureInPicture(
|
||||
isPlaying = playbackSnapshot.isPlaying,
|
||||
playerSize = layoutSize,
|
||||
|
|
@ -1101,6 +1115,12 @@ fun PlayerScreen(
|
|||
settings.streamAutoPlayPreferBingeGroup
|
||||
)
|
||||
|
||||
// bingeGroupOnly manual mode: only binge group preference is active (not next-episode toggle)
|
||||
val bingeGroupOnlyManualMode =
|
||||
shouldAutoSelectInManualMode &&
|
||||
!settings.streamAutoPlayNextEpisodeEnabled &&
|
||||
settings.streamAutoPlayPreferBingeGroup
|
||||
|
||||
// Determine auto-play mode for next episode
|
||||
val effectiveMode = if (shouldAutoSelectInManualMode) {
|
||||
StreamAutoPlayMode.FIRST_STREAM
|
||||
|
|
@ -1108,7 +1128,7 @@ fun PlayerScreen(
|
|||
settings.streamAutoPlayMode
|
||||
}
|
||||
val effectiveSource = if (shouldAutoSelectInManualMode) {
|
||||
com.nuvio.app.features.streams.StreamAutoPlaySource.ALL_SOURCES
|
||||
StreamAutoPlaySource.ALL_SOURCES
|
||||
} else {
|
||||
settings.streamAutoPlaySource
|
||||
}
|
||||
|
|
@ -1128,6 +1148,13 @@ fun PlayerScreen(
|
|||
settings.streamAutoPlayRegex
|
||||
}
|
||||
|
||||
// Determine preferred binge group from current stream (not cache)
|
||||
val preferredBingeGroup = if (settings.streamAutoPlayPreferBingeGroup) {
|
||||
currentStreamBingeGroup
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
nextEpisodeAutoPlayJob = scope.launch {
|
||||
PlayerStreamsRepository.loadEpisodeStreams(
|
||||
type = type,
|
||||
|
|
@ -1140,59 +1167,171 @@ fun PlayerScreen(
|
|||
.map { it.displayTitle }
|
||||
.toSet()
|
||||
|
||||
val timeoutMs = settings.streamAutoPlayTimeoutSeconds * 1000L
|
||||
val startTime = WatchProgressClock.nowEpochMs()
|
||||
val timeoutSeconds = settings.streamAutoPlayTimeoutSeconds
|
||||
val isUnlimitedTimeout = timeoutSeconds == Int.MAX_VALUE
|
||||
var autoSelectTriggered = false
|
||||
var timeoutElapsed = false
|
||||
var selectedStream: StreamItem? = null
|
||||
|
||||
// Collect streams as they arrive
|
||||
PlayerStreamsRepository.episodeStreamsState.collectLatest { state ->
|
||||
if (state.groups.isEmpty() && state.isAnyLoading) return@collectLatest
|
||||
// Full select: tries binge group first, then falls back to mode-based selection
|
||||
fun trySelectStream(streams: List<StreamItem>): StreamItem? {
|
||||
return StreamAutoPlaySelector.selectAutoPlayStream(
|
||||
streams = streams,
|
||||
mode = effectiveMode,
|
||||
regexPattern = effectiveRegex,
|
||||
source = effectiveSource,
|
||||
installedAddonNames = installedAddonNames,
|
||||
selectedAddons = effectiveSelectedAddons,
|
||||
selectedPlugins = effectiveSelectedPlugins,
|
||||
preferredBingeGroup = preferredBingeGroup,
|
||||
preferBingeGroupInSelection = settings.streamAutoPlayPreferBingeGroup,
|
||||
bingeGroupOnly = bingeGroupOnlyManualMode,
|
||||
)
|
||||
}
|
||||
|
||||
val allStreams = state.groups.flatMap { it.streams }
|
||||
val elapsed = WatchProgressClock.nowEpochMs() - startTime
|
||||
// Binge group only early match: returns null if no binge group match
|
||||
fun tryBingeGroupOnly(streams: List<StreamItem>): StreamItem? {
|
||||
if (preferredBingeGroup == null || !settings.streamAutoPlayPreferBingeGroup) return null
|
||||
return StreamAutoPlaySelector.selectAutoPlayStream(
|
||||
streams = streams,
|
||||
mode = effectiveMode,
|
||||
regexPattern = effectiveRegex,
|
||||
source = effectiveSource,
|
||||
installedAddonNames = installedAddonNames,
|
||||
selectedAddons = effectiveSelectedAddons,
|
||||
selectedPlugins = effectiveSelectedPlugins,
|
||||
preferredBingeGroup = preferredBingeGroup,
|
||||
preferBingeGroupInSelection = true,
|
||||
bingeGroupOnly = true,
|
||||
)
|
||||
}
|
||||
|
||||
val selected = if (allStreams.isNotEmpty()) {
|
||||
StreamAutoPlaySelector.selectAutoPlayStream(
|
||||
streams = allStreams,
|
||||
mode = effectiveMode,
|
||||
regexPattern = effectiveRegex,
|
||||
source = effectiveSource,
|
||||
installedAddonNames = installedAddonNames,
|
||||
selectedAddons = effectiveSelectedAddons,
|
||||
selectedPlugins = effectiveSelectedPlugins,
|
||||
preferredBingeGroup = if (settings.streamAutoPlayPreferBingeGroup) {
|
||||
currentStreamBingeGroup
|
||||
} else {
|
||||
null
|
||||
},
|
||||
preferBingeGroupInSelection = settings.streamAutoPlayPreferBingeGroup,
|
||||
)
|
||||
} else null
|
||||
val innerJob = launch {
|
||||
// Collect streams as they arrive
|
||||
PlayerStreamsRepository.episodeStreamsState.collectLatest { state ->
|
||||
if (state.groups.isEmpty() && state.isAnyLoading) return@collectLatest
|
||||
|
||||
if (selected != null || !state.isAnyLoading || elapsed >= timeoutMs) {
|
||||
nextEpisodeAutoPlaySearching = false
|
||||
if (selected != null) {
|
||||
nextEpisodeAutoPlaySourceName = selected.addonName
|
||||
// Countdown before playing
|
||||
for (i in 3 downTo 1) {
|
||||
nextEpisodeAutoPlayCountdown = i
|
||||
delay(1000)
|
||||
val allStreams = state.groups.flatMap { it.streams }
|
||||
|
||||
if (autoSelectTriggered) {
|
||||
// Already resolved
|
||||
} else if (timeoutElapsed) {
|
||||
// Timeout elapsed: full select (binge group + fallback to mode)
|
||||
if (allStreams.isNotEmpty()) {
|
||||
val candidate = trySelectStream(allStreams)
|
||||
if (candidate != null) {
|
||||
autoSelectTriggered = true
|
||||
selectedStream = candidate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Before timeout: eagerly check binge group only
|
||||
if (allStreams.isNotEmpty()) {
|
||||
val earlyMatch = tryBingeGroupOnly(allStreams)
|
||||
if (earlyMatch != null) {
|
||||
autoSelectTriggered = true
|
||||
selectedStream = earlyMatch
|
||||
}
|
||||
}
|
||||
switchToEpisodeStream(selected, nextVideo)
|
||||
showNextEpisodeCard = false
|
||||
nextEpisodeAutoPlayCountdown = null
|
||||
nextEpisodeAutoPlaySourceName = null
|
||||
} else if (!state.isAnyLoading || elapsed >= timeoutMs) {
|
||||
// No stream found — open the episode streams panel for manual selection
|
||||
episodeStreamsPanelState = EpisodeStreamsPanelState(
|
||||
showStreams = true,
|
||||
selectedEpisode = nextVideo,
|
||||
)
|
||||
showEpisodesPanel = true
|
||||
showNextEpisodeCard = false
|
||||
}
|
||||
return@collectLatest
|
||||
|
||||
// If all addons finished loading and no match yet, do a final full select
|
||||
if (!autoSelectTriggered && !state.isAnyLoading) {
|
||||
if (allStreams.isNotEmpty()) {
|
||||
val candidate = trySelectStream(allStreams)
|
||||
if (candidate != null) {
|
||||
autoSelectTriggered = true
|
||||
selectedStream = candidate
|
||||
}
|
||||
}
|
||||
if (!autoSelectTriggered) {
|
||||
autoSelectTriggered = true
|
||||
}
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
if (autoSelectTriggered) return@collectLatest
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout logic
|
||||
val timeoutMs = timeoutSeconds * 1_000L
|
||||
val isBoundedTimeout = timeoutSeconds in 1..30
|
||||
|
||||
if (isBoundedTimeout) {
|
||||
// Bounded timeout (1-30s): wait, then trigger full select
|
||||
delay(timeoutMs)
|
||||
timeoutElapsed = true
|
||||
if (!autoSelectTriggered) {
|
||||
val allStreams = PlayerStreamsRepository.episodeStreamsState.value.groups.flatMap { it.streams }
|
||||
if (allStreams.isNotEmpty()) {
|
||||
val candidate = trySelectStream(allStreams)
|
||||
if (candidate != null) {
|
||||
autoSelectTriggered = true
|
||||
selectedStream = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedStream != null) {
|
||||
innerJob.cancel()
|
||||
} else if (PlayerStreamsRepository.episodeStreamsState.value.groups.flatMap { it.streams }.isNotEmpty()) {
|
||||
// Streams arrived but no match after full select — don't wait further
|
||||
innerJob.cancel()
|
||||
autoSelectTriggered = true
|
||||
} else {
|
||||
// No addon responded yet — wait with hard ceiling
|
||||
val completed = withTimeoutOrNull(timeoutMs) { innerJob.join() }
|
||||
if (completed == null) {
|
||||
innerJob.cancel()
|
||||
if (!autoSelectTriggered) {
|
||||
val allStreams = PlayerStreamsRepository.episodeStreamsState.value.groups.flatMap { it.streams }
|
||||
if (allStreams.isNotEmpty()) {
|
||||
selectedStream = trySelectStream(allStreams)
|
||||
}
|
||||
autoSelectTriggered = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Instant (0) or unlimited: timeoutElapsed immediately so each
|
||||
// addon response triggers a full select attempt in the collect.
|
||||
timeoutElapsed = true
|
||||
val hardTimeout = NEXT_EPISODE_HARD_TIMEOUT_MS
|
||||
val completed = withTimeoutOrNull(hardTimeout) { innerJob.join() }
|
||||
if (completed == null) {
|
||||
innerJob.cancel()
|
||||
if (!autoSelectTriggered) {
|
||||
val allStreams = PlayerStreamsRepository.episodeStreamsState.value.groups.flatMap { it.streams }
|
||||
if (allStreams.isNotEmpty()) {
|
||||
selectedStream = trySelectStream(allStreams)
|
||||
}
|
||||
autoSelectTriggered = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle result
|
||||
nextEpisodeAutoPlaySearching = false
|
||||
if (selectedStream != null) {
|
||||
nextEpisodeAutoPlaySourceName = selectedStream!!.addonName
|
||||
// Countdown before playing
|
||||
for (i in 3 downTo 1) {
|
||||
nextEpisodeAutoPlayCountdown = i
|
||||
delay(1000)
|
||||
}
|
||||
switchToEpisodeStream(selectedStream!!, nextVideo)
|
||||
showNextEpisodeCard = false
|
||||
nextEpisodeAutoPlayCountdown = null
|
||||
nextEpisodeAutoPlaySourceName = null
|
||||
} else {
|
||||
// No stream found — open the episode streams panel for manual selection
|
||||
episodeStreamsPanelState = EpisodeStreamsPanelState(
|
||||
showStreams = true,
|
||||
selectedEpisode = nextVideo,
|
||||
)
|
||||
showEpisodesPanel = true
|
||||
showNextEpisodeCard = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1890,7 +2029,7 @@ fun PlayerScreen(
|
|||
// Skip intro/recap/outro button
|
||||
if (!playerControlsLocked) {
|
||||
SkipIntroButton(
|
||||
interval = activeSkipInterval,
|
||||
interval = if (!initialLoadCompleted || pausedOverlayVisible) null else activeSkipInterval,
|
||||
dismissed = skipIntervalDismissed,
|
||||
controlsVisible = controlsVisible,
|
||||
onSkip = {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,29 @@ import com.nuvio.app.features.streams.StreamAutoPlaySource
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlin.math.abs
|
||||
|
||||
val STREAM_AUTO_PLAY_TIMEOUT_VALUES: List<Int> = listOf(
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, Int.MAX_VALUE
|
||||
)
|
||||
|
||||
/**
|
||||
* Snaps [value] to the nearest allowed timeout value in [STREAM_AUTO_PLAY_TIMEOUT_VALUES].
|
||||
* Ties break to the lower value. Negative values snap to 0.
|
||||
*/
|
||||
fun snapToAllowedTimeout(value: Int): Int {
|
||||
if (value <= 0) return 0
|
||||
var bestValue = STREAM_AUTO_PLAY_TIMEOUT_VALUES[0]
|
||||
var bestDistance = Long.MAX_VALUE
|
||||
for (allowed in STREAM_AUTO_PLAY_TIMEOUT_VALUES) {
|
||||
val distance = abs(value.toLong() - allowed.toLong())
|
||||
if (distance < bestDistance || (distance == bestDistance && allowed < bestValue)) {
|
||||
bestDistance = distance
|
||||
bestValue = allowed
|
||||
}
|
||||
}
|
||||
return bestValue
|
||||
}
|
||||
|
||||
data class PlayerSettingsUiState(
|
||||
val showLoadingOverlay: Boolean = true,
|
||||
|
|
@ -38,6 +61,7 @@ data class PlayerSettingsUiState(
|
|||
val introSubmitEnabled: Boolean = false,
|
||||
val streamAutoPlayNextEpisodeEnabled: Boolean = false,
|
||||
val streamAutoPlayPreferBingeGroup: Boolean = true,
|
||||
val streamAutoPlayReuseBingeGroup: Boolean = true,
|
||||
val nextEpisodeThresholdMode: NextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE,
|
||||
val nextEpisodeThresholdPercent: Float = 99f,
|
||||
val nextEpisodeThresholdMinutesBeforeEnd: Float = 2f,
|
||||
|
|
@ -93,6 +117,7 @@ object PlayerSettingsRepository {
|
|||
private var introSubmitEnabled = false
|
||||
private var streamAutoPlayNextEpisodeEnabled = false
|
||||
private var streamAutoPlayPreferBingeGroup = true
|
||||
private var streamAutoPlayReuseBingeGroup = true
|
||||
private var nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE
|
||||
private var nextEpisodeThresholdPercent = 99f
|
||||
private var nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||
|
|
@ -153,6 +178,7 @@ object PlayerSettingsRepository {
|
|||
introSubmitEnabled = false
|
||||
streamAutoPlayNextEpisodeEnabled = false
|
||||
streamAutoPlayPreferBingeGroup = true
|
||||
streamAutoPlayReuseBingeGroup = true
|
||||
nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE
|
||||
nextEpisodeThresholdPercent = 99f
|
||||
nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||
|
|
@ -232,6 +258,14 @@ object PlayerSettingsRepository {
|
|||
}
|
||||
streamAutoPlayRegex = PlayerSettingsStorage.loadStreamAutoPlayRegex() ?: ""
|
||||
streamAutoPlayTimeoutSeconds = PlayerSettingsStorage.loadStreamAutoPlayTimeoutSeconds() ?: 3
|
||||
// Legacy migration: 11 was the old sentinel for "unlimited"
|
||||
if (streamAutoPlayTimeoutSeconds == 11) {
|
||||
streamAutoPlayTimeoutSeconds = Int.MAX_VALUE
|
||||
PlayerSettingsStorage.saveStreamAutoPlayTimeoutSeconds(streamAutoPlayTimeoutSeconds)
|
||||
} else if (streamAutoPlayTimeoutSeconds !in STREAM_AUTO_PLAY_TIMEOUT_VALUES) {
|
||||
streamAutoPlayTimeoutSeconds = snapToAllowedTimeout(streamAutoPlayTimeoutSeconds)
|
||||
PlayerSettingsStorage.saveStreamAutoPlayTimeoutSeconds(streamAutoPlayTimeoutSeconds)
|
||||
}
|
||||
skipIntroEnabled = PlayerSettingsStorage.loadSkipIntroEnabled() ?: true
|
||||
animeSkipEnabled = PlayerSettingsStorage.loadAnimeSkipEnabled() ?: false
|
||||
animeSkipClientId = PlayerSettingsStorage.loadAnimeSkipClientId() ?: ""
|
||||
|
|
@ -239,6 +273,7 @@ object PlayerSettingsRepository {
|
|||
introSubmitEnabled = PlayerSettingsStorage.loadIntroSubmitEnabled() ?: false
|
||||
streamAutoPlayNextEpisodeEnabled = PlayerSettingsStorage.loadStreamAutoPlayNextEpisodeEnabled() ?: false
|
||||
streamAutoPlayPreferBingeGroup = PlayerSettingsStorage.loadStreamAutoPlayPreferBingeGroup() ?: true
|
||||
streamAutoPlayReuseBingeGroup = PlayerSettingsStorage.loadStreamAutoPlayReuseBingeGroup() ?: true
|
||||
nextEpisodeThresholdMode = PlayerSettingsStorage.loadNextEpisodeThresholdMode()
|
||||
?.let { runCatching { NextEpisodeThresholdMode.valueOf(it) }.getOrNull() }
|
||||
?: NextEpisodeThresholdMode.PERCENTAGE
|
||||
|
|
@ -524,6 +559,14 @@ object PlayerSettingsRepository {
|
|||
PlayerSettingsStorage.saveStreamAutoPlayPreferBingeGroup(enabled)
|
||||
}
|
||||
|
||||
fun setStreamAutoPlayReuseBingeGroup(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
if (streamAutoPlayReuseBingeGroup == enabled) return
|
||||
streamAutoPlayReuseBingeGroup = enabled
|
||||
publish()
|
||||
PlayerSettingsStorage.saveStreamAutoPlayReuseBingeGroup(enabled)
|
||||
}
|
||||
|
||||
fun setNextEpisodeThresholdMode(mode: NextEpisodeThresholdMode) {
|
||||
ensureLoaded()
|
||||
if (nextEpisodeThresholdMode == mode) return
|
||||
|
|
@ -753,6 +796,7 @@ object PlayerSettingsRepository {
|
|||
introSubmitEnabled = introSubmitEnabled,
|
||||
streamAutoPlayNextEpisodeEnabled = streamAutoPlayNextEpisodeEnabled,
|
||||
streamAutoPlayPreferBingeGroup = streamAutoPlayPreferBingeGroup,
|
||||
streamAutoPlayReuseBingeGroup = streamAutoPlayReuseBingeGroup,
|
||||
nextEpisodeThresholdMode = nextEpisodeThresholdMode,
|
||||
nextEpisodeThresholdPercent = nextEpisodeThresholdPercent,
|
||||
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ internal expect object PlayerSettingsStorage {
|
|||
fun saveStreamAutoPlayNextEpisodeEnabled(enabled: Boolean)
|
||||
fun loadStreamAutoPlayPreferBingeGroup(): Boolean?
|
||||
fun saveStreamAutoPlayPreferBingeGroup(enabled: Boolean)
|
||||
fun loadStreamAutoPlayReuseBingeGroup(): Boolean?
|
||||
fun saveStreamAutoPlayReuseBingeGroup(enabled: Boolean)
|
||||
fun loadNextEpisodeThresholdMode(): String?
|
||||
fun saveNextEpisodeThresholdMode(mode: String)
|
||||
fun loadNextEpisodeThresholdPercent(): Float?
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import com.nuvio.app.features.player.IosHardwareDecoderMode
|
|||
import com.nuvio.app.features.player.IosTargetPrimaries
|
||||
import com.nuvio.app.features.player.IosTargetTransfer
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
import com.nuvio.app.features.player.STREAM_AUTO_PLAY_TIMEOUT_VALUES
|
||||
import com.nuvio.app.features.player.SubtitleLanguageOption
|
||||
import com.nuvio.app.features.player.formatPlaybackSpeedLabel
|
||||
import com.nuvio.app.features.player.languageLabelForCode
|
||||
|
|
@ -365,7 +366,7 @@ private fun PlaybackSettingsSection(
|
|||
val timeoutSec = autoPlayPlayerSettings.streamAutoPlayTimeoutSeconds
|
||||
val timeoutLabel = when (timeoutSec) {
|
||||
0 -> stringResource(Res.string.settings_playback_timeout_instant)
|
||||
11 -> stringResource(Res.string.settings_playback_timeout_unlimited)
|
||||
Int.MAX_VALUE -> stringResource(Res.string.settings_playback_timeout_unlimited)
|
||||
else -> stringResource(Res.string.settings_playback_timeout_seconds, timeoutSec)
|
||||
}
|
||||
Column(
|
||||
|
|
@ -391,8 +392,11 @@ private fun PlaybackSettingsSection(
|
|||
}
|
||||
ValueBox(text = timeoutLabel, modifier = Modifier.wrapContentWidth())
|
||||
}
|
||||
var sliderValue by remember(timeoutSec) { mutableFloatStateOf(timeoutSec.toFloat()) }
|
||||
var lastHapticStep by remember(timeoutSec) { mutableStateOf(timeoutSec.toFloat()) }
|
||||
val timeoutIndex = STREAM_AUTO_PLAY_TIMEOUT_VALUES.indexOf(timeoutSec)
|
||||
.coerceAtLeast(0)
|
||||
val maxIndex = (STREAM_AUTO_PLAY_TIMEOUT_VALUES.size - 1).toFloat()
|
||||
var sliderValue by remember(timeoutIndex) { mutableFloatStateOf(timeoutIndex.toFloat()) }
|
||||
var lastHapticStep by remember(timeoutIndex) { mutableStateOf(timeoutIndex.toFloat()) }
|
||||
Slider(
|
||||
value = sliderValue,
|
||||
onValueChange = {
|
||||
|
|
@ -405,10 +409,11 @@ private fun PlaybackSettingsSection(
|
|||
}
|
||||
},
|
||||
onValueChangeFinished = {
|
||||
PlayerSettingsRepository.setStreamAutoPlayTimeoutSeconds(sliderValue.toInt())
|
||||
val index = sliderValue.toInt().coerceIn(0, STREAM_AUTO_PLAY_TIMEOUT_VALUES.size - 1)
|
||||
PlayerSettingsRepository.setStreamAutoPlayTimeoutSeconds(STREAM_AUTO_PLAY_TIMEOUT_VALUES[index])
|
||||
},
|
||||
valueRange = 0f..11f,
|
||||
steps = calculateSteps(0f, 11f, 1f),
|
||||
valueRange = 0f..maxIndex,
|
||||
steps = calculateSteps(0f, maxIndex, 1f),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = MaterialTheme.colorScheme.primary,
|
||||
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||
|
|
@ -658,6 +663,16 @@ private fun PlaybackSettingsSection(
|
|||
isTablet = isTablet,
|
||||
onCheckedChange = PlayerSettingsRepository::setStreamAutoPlayPreferBingeGroup,
|
||||
)
|
||||
if (autoPlayPlayerSettings.streamAutoPlayPreferBingeGroup) {
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsSwitchRow(
|
||||
title = stringResource(Res.string.settings_playback_reuse_binge_group),
|
||||
description = stringResource(Res.string.settings_playback_reuse_binge_group_description),
|
||||
checked = autoPlayPlayerSettings.streamAutoPlayReuseBingeGroup,
|
||||
isTablet = isTablet,
|
||||
onCheckedChange = PlayerSettingsRepository::setStreamAutoPlayReuseBingeGroup,
|
||||
)
|
||||
}
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
var showThresholdModeDialog by remember { mutableStateOf(false) }
|
||||
SettingsNavigationRow(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
object BingeGroupCacheRepository {
|
||||
|
||||
fun save(contentId: String, bingeGroup: String) {
|
||||
BingeGroupCacheStorage.save(hashedKey(contentId), bingeGroup)
|
||||
}
|
||||
|
||||
fun get(contentId: String): String? {
|
||||
return BingeGroupCacheStorage.load(hashedKey(contentId))
|
||||
}
|
||||
|
||||
fun remove(contentId: String) {
|
||||
BingeGroupCacheStorage.remove(hashedKey(contentId))
|
||||
}
|
||||
|
||||
private fun hashedKey(contentId: String): String {
|
||||
val hash = contentId.fold(0L) { acc, c -> acc * 31 + c.code }.toULong()
|
||||
return "binge_group_$hash"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
internal expect object BingeGroupCacheStorage {
|
||||
fun load(hashedKey: String): String?
|
||||
fun save(hashedKey: String, value: String)
|
||||
fun remove(hashedKey: String)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.nuvio.app.features.player.PlayerSettingsUiState
|
|||
object StreamAutoPlayPolicy {
|
||||
fun isEffectivelyEnabled(settings: PlayerSettingsUiState): Boolean {
|
||||
if (settings.streamReuseLastLinkEnabled) return true
|
||||
if (settings.streamAutoPlayReuseBingeGroup && settings.streamAutoPlayPreferBingeGroup) return true
|
||||
|
||||
return when (settings.streamAutoPlayMode) {
|
||||
StreamAutoPlayMode.MANUAL -> false
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ object StreamAutoPlaySelector {
|
|||
selectedPlugins: Set<String>,
|
||||
preferredBingeGroup: String? = null,
|
||||
preferBingeGroupInSelection: Boolean = false,
|
||||
bingeGroupOnly: Boolean = false,
|
||||
): StreamItem? {
|
||||
if (streams.isEmpty()) return null
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ object StreamAutoPlaySelector {
|
|||
}
|
||||
}
|
||||
if (candidateStreams.isEmpty()) return null
|
||||
if (mode == StreamAutoPlayMode.MANUAL) return null
|
||||
if (mode == StreamAutoPlayMode.MANUAL && !bingeGroupOnly) return null
|
||||
|
||||
val targetBingeGroup = preferredBingeGroup?.trim().orEmpty()
|
||||
if (preferBingeGroupInSelection && targetBingeGroup.isNotEmpty()) {
|
||||
|
|
@ -65,6 +66,12 @@ object StreamAutoPlaySelector {
|
|||
stream.behaviorHints.bingeGroup == targetBingeGroup && stream.isAutoPlayable()
|
||||
}
|
||||
if (bingeGroupMatch != null) return bingeGroupMatch
|
||||
// When bingeGroupOnly = true, do NOT fall through to mode-based selection
|
||||
if (bingeGroupOnly) return null
|
||||
} else if (bingeGroupOnly) {
|
||||
// bingeGroupOnly requested but no preferredBingeGroup or preferBingeGroupInSelection is false
|
||||
// Fall through to mode-based selection (bingeGroupOnly has no effect without a binge group to match)
|
||||
if (mode == StreamAutoPlayMode.MANUAL) return null
|
||||
}
|
||||
|
||||
return when (mode) {
|
||||
|
|
|
|||
|
|
@ -48,10 +48,11 @@ object StreamsRepository {
|
|||
): String =
|
||||
"$type::$videoId::$season::$episode::$manualSelection"
|
||||
|
||||
fun load(type: String, videoId: String, season: Int? = null, episode: Int? = null, manualSelection: Boolean = false) {
|
||||
fun load(type: String, videoId: String, parentMetaId: String? = null, season: Int? = null, episode: Int? = null, manualSelection: Boolean = false) {
|
||||
load(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
season = season,
|
||||
episode = episode,
|
||||
manualSelection = manualSelection,
|
||||
|
|
@ -59,10 +60,11 @@ object StreamsRepository {
|
|||
)
|
||||
}
|
||||
|
||||
fun reload(type: String, videoId: String, season: Int? = null, episode: Int? = null, manualSelection: Boolean = false) {
|
||||
fun reload(type: String, videoId: String, parentMetaId: String? = null, season: Int? = null, episode: Int? = null, manualSelection: Boolean = false) {
|
||||
load(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
season = season,
|
||||
episode = episode,
|
||||
manualSelection = manualSelection,
|
||||
|
|
@ -70,7 +72,7 @@ object StreamsRepository {
|
|||
)
|
||||
}
|
||||
|
||||
private fun load(type: String, videoId: String, season: Int?, episode: Int?, manualSelection: Boolean, forceRefresh: Boolean) {
|
||||
private fun load(type: String, videoId: String, parentMetaId: String?, season: Int?, episode: Int?, manualSelection: Boolean, forceRefresh: Boolean) {
|
||||
val pluginUiState = if (AppFeaturePolicy.pluginsEnabled) {
|
||||
PluginRepository.initialize()
|
||||
PluginRepository.uiState.value
|
||||
|
|
@ -105,7 +107,21 @@ object StreamsRepository {
|
|||
val isAutoPlayEnabled = !manualSelection && autoPlayMode != StreamAutoPlayMode.MANUAL &&
|
||||
!(autoPlayMode == StreamAutoPlayMode.REGEX_MATCH &&
|
||||
!StreamAutoPlayPolicy.isRegexSelectionConfigured(playerSettings.streamAutoPlayRegex))
|
||||
val isDirectAutoPlayFlow = isAutoPlayEnabled
|
||||
|
||||
// Look up persisted binge group when both settings are enabled
|
||||
val persistedBingeGroup = if (
|
||||
playerSettings.streamAutoPlayPreferBingeGroup &&
|
||||
playerSettings.streamAutoPlayReuseBingeGroup
|
||||
) {
|
||||
parentMetaId?.let { BingeGroupCacheRepository.get(it) }
|
||||
} else null
|
||||
|
||||
// Enable direct auto-play flow if normal auto-play is enabled,
|
||||
// OR if we have a persisted binge group in MANUAL mode
|
||||
val bingeGroupDirectFlow = !manualSelection &&
|
||||
persistedBingeGroup != null &&
|
||||
autoPlayMode == StreamAutoPlayMode.MANUAL
|
||||
val isDirectAutoPlayFlow = isAutoPlayEnabled || bingeGroupDirectFlow
|
||||
|
||||
if (isDirectAutoPlayFlow) {
|
||||
_uiState.value = StreamsUiState(
|
||||
|
|
@ -237,16 +253,59 @@ object StreamsRepository {
|
|||
}
|
||||
}
|
||||
|
||||
val timeoutJob = if (isAutoPlayEnabled) {
|
||||
val timeoutMs = playerSettings.streamAutoPlayTimeoutSeconds * 1_000L
|
||||
if (timeoutMs > 0L && playerSettings.streamAutoPlayTimeoutSeconds < 11) {
|
||||
val timeoutJob = if (isDirectAutoPlayFlow) {
|
||||
val timeoutSeconds = playerSettings.streamAutoPlayTimeoutSeconds
|
||||
val isUnlimitedTimeout = timeoutSeconds == Int.MAX_VALUE
|
||||
// Timeout semantics:
|
||||
// - 0 (instant): timeoutElapsed immediately, full select on each response
|
||||
// - 1-30 (bounded): wait the configured delay, then full select
|
||||
// - unlimited (Int.MAX_VALUE): timeoutElapsed immediately, full select on each response,
|
||||
// with 60s hard fallback to stream picker
|
||||
if (timeoutSeconds <= 0 || isUnlimitedTimeout) {
|
||||
timeoutElapsed = true
|
||||
// For unlimited: launch a hard 60s fallback to dismiss overlay
|
||||
if (isUnlimitedTimeout) {
|
||||
launch {
|
||||
delay(60_000L)
|
||||
if (!autoSelectTriggered) {
|
||||
autoSelectTriggered = true
|
||||
val allStreams = _uiState.value.groups.flatMap { it.streams }
|
||||
if (allStreams.isNotEmpty()) {
|
||||
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
|
||||
streams = allStreams,
|
||||
mode = autoPlayMode,
|
||||
regexPattern = playerSettings.streamAutoPlayRegex,
|
||||
source = playerSettings.streamAutoPlaySource,
|
||||
installedAddonNames = installedAddonNames,
|
||||
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
||||
selectedPlugins = playerSettings.streamAutoPlaySelectedPlugins,
|
||||
preferredBingeGroup = persistedBingeGroup,
|
||||
preferBingeGroupInSelection = persistedBingeGroup != null,
|
||||
bingeGroupOnly = false,
|
||||
)
|
||||
_uiState.update { it.copy(autoPlayStream = selected) }
|
||||
}
|
||||
if (_uiState.value.autoPlayStream == null) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isDirectAutoPlayFlow = false,
|
||||
showDirectAutoPlayOverlay = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
// Bounded timeout (1-30s)
|
||||
launch {
|
||||
delay(timeoutMs)
|
||||
delay(timeoutSeconds * 1_000L)
|
||||
timeoutElapsed = true
|
||||
if (!autoSelectTriggered) {
|
||||
val allStreams = _uiState.value.groups.flatMap { it.streams }
|
||||
if (allStreams.isNotEmpty()) {
|
||||
autoSelectTriggered = true
|
||||
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
|
||||
streams = allStreams,
|
||||
mode = autoPlayMode,
|
||||
|
|
@ -255,9 +314,14 @@ object StreamsRepository {
|
|||
installedAddonNames = installedAddonNames,
|
||||
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
||||
selectedPlugins = playerSettings.streamAutoPlaySelectedPlugins,
|
||||
preferredBingeGroup = persistedBingeGroup,
|
||||
preferBingeGroupInSelection = persistedBingeGroup != null,
|
||||
bingeGroupOnly = false,
|
||||
)
|
||||
_uiState.update { it.copy(autoPlayStream = selected) }
|
||||
if (selected == null) {
|
||||
if (selected != null) {
|
||||
autoSelectTriggered = true
|
||||
_uiState.update { it.copy(autoPlayStream = selected) }
|
||||
} else {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isDirectAutoPlayFlow = false,
|
||||
|
|
@ -268,11 +332,6 @@ object StreamsRepository {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (timeoutMs <= 0L) {
|
||||
timeoutElapsed = true
|
||||
null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
|
|
@ -479,9 +538,54 @@ object StreamsRepository {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Early match / timeout-elapsed auto-select on each addon response
|
||||
if (isDirectAutoPlayFlow && !autoSelectTriggered) {
|
||||
val allStreams = _uiState.value.groups.flatMap { it.streams }
|
||||
if (allStreams.isNotEmpty()) {
|
||||
if (timeoutElapsed) {
|
||||
// After timeout: full fallback (bingeGroupOnly = false)
|
||||
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
|
||||
streams = allStreams,
|
||||
mode = autoPlayMode,
|
||||
regexPattern = playerSettings.streamAutoPlayRegex,
|
||||
source = playerSettings.streamAutoPlaySource,
|
||||
installedAddonNames = installedAddonNames,
|
||||
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
||||
selectedPlugins = playerSettings.streamAutoPlaySelectedPlugins,
|
||||
preferredBingeGroup = persistedBingeGroup,
|
||||
preferBingeGroupInSelection = persistedBingeGroup != null,
|
||||
bingeGroupOnly = false,
|
||||
)
|
||||
if (selected != null) {
|
||||
autoSelectTriggered = true
|
||||
_uiState.update { it.copy(autoPlayStream = selected) }
|
||||
}
|
||||
} else if (persistedBingeGroup != null) {
|
||||
// Before timeout: try binge-group-only early match
|
||||
val earlyMatch = StreamAutoPlaySelector.selectAutoPlayStream(
|
||||
streams = allStreams,
|
||||
mode = autoPlayMode,
|
||||
regexPattern = playerSettings.streamAutoPlayRegex,
|
||||
source = playerSettings.streamAutoPlaySource,
|
||||
installedAddonNames = installedAddonNames,
|
||||
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
||||
selectedPlugins = playerSettings.streamAutoPlaySelectedPlugins,
|
||||
preferredBingeGroup = persistedBingeGroup,
|
||||
preferBingeGroupInSelection = true,
|
||||
bingeGroupOnly = true,
|
||||
)
|
||||
if (earlyMatch != null) {
|
||||
autoSelectTriggered = true
|
||||
_uiState.update { it.copy(autoPlayStream = earlyMatch) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isAutoPlayEnabled && !autoSelectTriggered) {
|
||||
// All addons finished — run final auto-select if not yet triggered
|
||||
if (isDirectAutoPlayFlow && !autoSelectTriggered) {
|
||||
autoSelectTriggered = true
|
||||
val allStreams = _uiState.value.groups.flatMap { it.streams }
|
||||
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
|
||||
|
|
@ -492,6 +596,9 @@ object StreamsRepository {
|
|||
installedAddonNames = installedAddonNames,
|
||||
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
||||
selectedPlugins = playerSettings.streamAutoPlaySelectedPlugins,
|
||||
preferredBingeGroup = persistedBingeGroup,
|
||||
preferBingeGroupInSelection = persistedBingeGroup != null,
|
||||
bingeGroupOnly = false,
|
||||
)
|
||||
_uiState.update { it.copy(autoPlayStream = selected) }
|
||||
}
|
||||
|
|
@ -512,6 +619,7 @@ object StreamsRepository {
|
|||
}
|
||||
|
||||
fun consumeAutoPlay() {
|
||||
activeRequestKey = null
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
autoPlayStream = null,
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ fun StreamsScreen(
|
|||
StreamsRepository.load(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
season = seasonNumber,
|
||||
episode = episodeNumber,
|
||||
manualSelection = manualSelection,
|
||||
|
|
@ -277,6 +278,7 @@ fun StreamsScreen(
|
|||
StreamsRepository.reload(
|
||||
type = type,
|
||||
videoId = videoId,
|
||||
parentMetaId = parentMetaId,
|
||||
season = seasonNumber,
|
||||
episode = episodeNumber,
|
||||
manualSelection = manualSelection,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ actual object PlayerSettingsStorage {
|
|||
private const val introSubmitEnabledKey = "intro_submit_enabled"
|
||||
private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled"
|
||||
private const val streamAutoPlayPreferBingeGroupKey = "stream_auto_play_prefer_binge_group"
|
||||
private const val streamAutoPlayReuseBingeGroupKey = "stream_auto_play_reuse_binge_group"
|
||||
private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode"
|
||||
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
|
||||
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||
|
|
@ -99,6 +100,7 @@ actual object PlayerSettingsStorage {
|
|||
animeSkipClientIdKey,
|
||||
streamAutoPlayNextEpisodeEnabledKey,
|
||||
streamAutoPlayPreferBingeGroupKey,
|
||||
streamAutoPlayReuseBingeGroupKey,
|
||||
nextEpisodeThresholdModeKey,
|
||||
nextEpisodeThresholdPercentKey,
|
||||
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||
|
|
@ -554,6 +556,20 @@ actual object PlayerSettingsStorage {
|
|||
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(streamAutoPlayPreferBingeGroupKey))
|
||||
}
|
||||
|
||||
actual fun loadStreamAutoPlayReuseBingeGroup(): Boolean? {
|
||||
val defaults = NSUserDefaults.standardUserDefaults
|
||||
val key = ProfileScopedKey.of(streamAutoPlayReuseBingeGroupKey)
|
||||
return if (defaults.objectForKey(key) != null) {
|
||||
defaults.boolForKey(key)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
actual fun saveStreamAutoPlayReuseBingeGroup(enabled: Boolean) {
|
||||
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(streamAutoPlayReuseBingeGroupKey))
|
||||
}
|
||||
|
||||
actual fun loadNextEpisodeThresholdMode(): String? {
|
||||
val defaults = NSUserDefaults.standardUserDefaults
|
||||
val key = ProfileScopedKey.of(nextEpisodeThresholdModeKey)
|
||||
|
|
@ -725,6 +741,7 @@ actual object PlayerSettingsStorage {
|
|||
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
|
||||
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
|
||||
loadStreamAutoPlayReuseBingeGroup()?.let { put(streamAutoPlayReuseBingeGroupKey, encodeSyncBoolean(it)) }
|
||||
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
|
||||
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
|
||||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||
|
|
@ -782,6 +799,7 @@ actual object PlayerSettingsStorage {
|
|||
payload.decodeSyncString(introDbApiKeyKey)?.let(::saveIntroDbApiKey)
|
||||
payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
|
||||
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
|
||||
payload.decodeSyncBoolean(streamAutoPlayReuseBingeGroupKey)?.let(::saveStreamAutoPlayReuseBingeGroup)
|
||||
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
|
||||
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
|
||||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
actual object BingeGroupCacheStorage {
|
||||
actual fun load(hashedKey: String): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(hashedKey))
|
||||
|
||||
actual fun save(hashedKey: String, value: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(value, forKey = ProfileScopedKey.of(hashedKey))
|
||||
}
|
||||
|
||||
actual fun remove(hashedKey: String) {
|
||||
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(hashedKey))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue