mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-23 18:22:05 +00:00
Merge branch 'cmp-rewrite' of https://github.com/NuvioMedia/NuvioMobile into cmp-rewrite
This commit is contained in:
commit
88ea47d2f0
18 changed files with 694 additions and 71 deletions
|
|
@ -44,6 +44,7 @@ import com.nuvio.app.features.updater.AndroidAppUpdaterPlatform
|
||||||
import com.nuvio.app.core.ui.PosterCardStyleStorage
|
import com.nuvio.app.core.ui.PosterCardStyleStorage
|
||||||
import com.nuvio.app.features.watched.WatchedStorage
|
import com.nuvio.app.features.watched.WatchedStorage
|
||||||
import com.nuvio.app.features.streams.StreamLinkCacheStorage
|
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.ContinueWatchingEnrichmentStorage
|
||||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
|
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
|
||||||
import com.nuvio.app.features.watchprogress.ResumePromptStorage
|
import com.nuvio.app.features.watchprogress.ResumePromptStorage
|
||||||
|
|
@ -87,6 +88,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
EpisodeReleaseNotificationsStorage.initialize(applicationContext)
|
EpisodeReleaseNotificationsStorage.initialize(applicationContext)
|
||||||
WatchProgressStorage.initialize(applicationContext)
|
WatchProgressStorage.initialize(applicationContext)
|
||||||
StreamLinkCacheStorage.initialize(applicationContext)
|
StreamLinkCacheStorage.initialize(applicationContext)
|
||||||
|
BingeGroupCacheStorage.initialize(applicationContext)
|
||||||
PluginStorage.initialize(applicationContext)
|
PluginStorage.initialize(applicationContext)
|
||||||
CollectionMobileSettingsStorage.initialize(applicationContext)
|
CollectionMobileSettingsStorage.initialize(applicationContext)
|
||||||
CollectionStorage.initialize(applicationContext)
|
CollectionStorage.initialize(applicationContext)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ actual object PlayerSettingsStorage {
|
||||||
private const val introSubmitEnabledKey = "intro_submit_enabled"
|
private const val introSubmitEnabledKey = "intro_submit_enabled"
|
||||||
private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled"
|
private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled"
|
||||||
private const val streamAutoPlayPreferBingeGroupKey = "stream_auto_play_prefer_binge_group"
|
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 nextEpisodeThresholdModeKey = "next_episode_threshold_mode"
|
||||||
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
|
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
|
||||||
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||||
|
|
@ -101,6 +102,7 @@ actual object PlayerSettingsStorage {
|
||||||
animeSkipClientIdKey,
|
animeSkipClientIdKey,
|
||||||
streamAutoPlayNextEpisodeEnabledKey,
|
streamAutoPlayNextEpisodeEnabledKey,
|
||||||
streamAutoPlayPreferBingeGroupKey,
|
streamAutoPlayPreferBingeGroupKey,
|
||||||
|
streamAutoPlayReuseBingeGroupKey,
|
||||||
nextEpisodeThresholdModeKey,
|
nextEpisodeThresholdModeKey,
|
||||||
nextEpisodeThresholdPercentKey,
|
nextEpisodeThresholdPercentKey,
|
||||||
nextEpisodeThresholdMinutesBeforeEndKey,
|
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||||
|
|
@ -609,6 +611,23 @@ actual object PlayerSettingsStorage {
|
||||||
?.apply()
|
?.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? =
|
actual fun loadNextEpisodeThresholdMode(): String? =
|
||||||
preferences?.getString(ProfileScopedKey.of(nextEpisodeThresholdModeKey), null)
|
preferences?.getString(ProfileScopedKey.of(nextEpisodeThresholdModeKey), null)
|
||||||
|
|
||||||
|
|
@ -825,6 +844,7 @@ actual object PlayerSettingsStorage {
|
||||||
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
|
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
|
||||||
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
|
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
|
||||||
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
|
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
|
||||||
|
loadStreamAutoPlayReuseBingeGroup()?.let { put(streamAutoPlayReuseBingeGroupKey, encodeSyncBoolean(it)) }
|
||||||
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
|
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
|
||||||
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
|
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
|
||||||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||||
|
|
@ -883,6 +903,7 @@ actual object PlayerSettingsStorage {
|
||||||
payload.decodeSyncBoolean(introSubmitEnabledKey)?.let(::saveIntroSubmitEnabled)
|
payload.decodeSyncBoolean(introSubmitEnabledKey)?.let(::saveIntroSubmitEnabled)
|
||||||
payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
|
payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
|
||||||
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
|
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
|
||||||
|
payload.decodeSyncBoolean(streamAutoPlayReuseBingeGroupKey)?.let(::saveStreamAutoPlayReuseBingeGroup)
|
||||||
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
|
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
|
||||||
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
|
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
|
||||||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<resources>
|
<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="about_supporters_contributors_subtitle">Osoby wspierające i współtworzące projekt</string>
|
||||||
<string name="action_back">Wstecz</string>
|
<string name="action_back">Wstecz</string>
|
||||||
<string name="action_cancel">Anuluj</string>
|
<string name="action_cancel">Anuluj</string>
|
||||||
|
|
@ -17,6 +18,8 @@
|
||||||
<string name="action_resume">Wznów</string>
|
<string name="action_resume">Wznów</string>
|
||||||
<string name="action_retry">Ponów</string>
|
<string name="action_retry">Ponów</string>
|
||||||
<string name="action_save">Zapisz</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_installing">Instalowanie</string>
|
||||||
<string name="addon_title">Dodatki</string>
|
<string name="addon_title">Dodatki</string>
|
||||||
<string name="addons_badge_active">Aktywny</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_production_mode">Produkcja</string>
|
||||||
<string name="collections_editor_tmdb_network_mode">Stacja</string>
|
<string name="collections_editor_tmdb_network_mode">Stacja</string>
|
||||||
<string name="collections_editor_tmdb_collection_mode">Kolekcja</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_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_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_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_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_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_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_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_public_list">Publiczna lista TMDB</string>
|
||||||
<string name="collections_editor_tmdb_network_id">ID stacji</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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_search_results">Wyniki wyszukiwania</string>
|
||||||
<string name="collections_editor_tmdb_collection">Kolekcja TMDB</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_presets">Szablony</string>
|
||||||
<string name="collections_editor_tmdb_search">Szukaj</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_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_action">Akcja</string>
|
||||||
<string name="collections_editor_tmdb_genre_adventure">Przygodowy</string>
|
<string name="collections_editor_tmdb_genre_adventure">Przygodowy</string>
|
||||||
<string name="collections_editor_tmdb_genre_animation">Animacja</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_disney_plus">Disney+</string>
|
||||||
<string name="collections_editor_tmdb_network_prime_video">Prime Video</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_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_popular">Popularne</string>
|
||||||
<string name="collections_editor_tmdb_sort_top_rated">Najwyżej oceniane</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_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_list">Lista TMDB</string>
|
||||||
<string name="collections_editor_tmdb_subtitle_movie_collection">Kolekcja filmów 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_production">Produkcja</string>
|
||||||
<string name="collections_editor_tmdb_subtitle_network">Stacja</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_editor_tmdb_subtitle_discover">TMDB Discover</string>
|
||||||
<string name="collections_empty_subtitle">Utwórz jedną, aby uporządkować katalogi.</string>
|
<string name="collections_empty_subtitle">Utwórz jedną, aby uporządkować katalogi.</string>
|
||||||
<string name="collections_empty_title">Brak kolekcji</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_appearance">Wygląd</string>
|
||||||
<string name="compose_settings_page_content_discovery">Treści i odkrywanie</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_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_homescreen">Ekran główny</string>
|
||||||
<string name="compose_settings_page_integrations">Integracje</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_mdblist_ratings">Oceny MDBList</string>
|
||||||
<string name="compose_settings_page_meta_screen">Ekran metadanych</string>
|
<string name="compose_settings_page_meta_screen">Ekran metadanych</string>
|
||||||
<string name="compose_settings_page_notifications">Powiadomienia</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_description">Przełącz na inny profil.</string>
|
||||||
<string name="compose_settings_root_switch_profile_title">Przełącz 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="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_loading">Ładowanie list Trakt…</string>
|
||||||
<string name="compose_trakt_list_picker_subtitle">Wybierz, gdzie zapisać ten tytuł w Trakt</string>
|
<string name="compose_trakt_list_picker_subtitle">Wybierz, gdzie zapisać ten tytuł w Trakt</string>
|
||||||
<string name="action_donate">Wesprzyj</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">Język aplikacji</string>
|
||||||
<string name="settings_appearance_app_language_sheet_title">Wybierz język</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_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_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_display">WYŚWIETLANIE</string>
|
||||||
<string name="settings_appearance_section_home">EKRAN GŁÓWNY</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_selected_count">%1$d z %2$d wybranych</string>
|
||||||
<string name="settings_homescreen_show_hero">Pokaż Hero</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="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">%1$d z %2$d katalogów widocznych • %3$d źródeł hero wybranych</string>
|
||||||
<string name="settings_homescreen_summary_hint">Otwórz katalog tylko wtedy, gdy chcesz zmienić jego nazwę lub kolejność.</string>
|
<string name="settings_homescreen_summary_hint">Otwórz katalog tylko wtedy, gdy chcesz zmienić jego nazwę lub kolejność.</string>
|
||||||
<string name="settings_homescreen_visible">Widoczne</string>
|
<string name="settings_homescreen_visible">Widoczne</string>
|
||||||
|
<string name="settings_hide_secret">Ukryj wartość</string>
|
||||||
<string name="settings_playback_subtitle">Odtwarzacz, napisy i automatyczne odtwarzanie</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_radius">Zaokrąglenie karty</string>
|
||||||
<string name="settings_poster_card_style">STYL KARTY PLAKATU</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_dense">Gęsty</string>
|
||||||
<string name="settings_poster_width_large">Duży</string>
|
<string name="settings_poster_width_large">Duży</string>
|
||||||
<string name="settings_poster_width_standard">Standardowy</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_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_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_card_style">STYL KARTY</string>
|
||||||
<string name="settings_continue_watching_section_on_launch">PRZY URUCHOMIENIU</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>
|
<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_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_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_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_home">EKRAN GŁÓWNY</string>
|
||||||
<string name="settings_content_discovery_section_sources">ŹRÓDŁA</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>
|
<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_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_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_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_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_description">Pobierz klucz z https://mdblist.com/preferences i wklej go tutaj.</string>
|
||||||
<string name="settings_mdb_api_key_label">Klucz API</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_episode_style_list_description">Karty ze szczegółami na pierwszym planie</string>
|
||||||
<string name="settings_meta_episodes">Odcinki</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_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_group_label">Grupa %1$d</string>
|
||||||
<string name="settings_meta_more_like_this">Podobne</string>
|
<string name="settings_meta_more_like_this">Podobne</string>
|
||||||
<string name="settings_meta_more_like_this_description">Wiersz rekomendacji.</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">Anime Skip</string>
|
||||||
<string name="settings_playback_anime_skip_client_id">ID klienta AnimeSkip</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_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_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">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>
|
<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_duration_hours">%1$d godzin</string>
|
||||||
<string name="settings_playback_enable_libass">Włącz libass</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_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_speed">Prędkość przy przytrzymaniu</string>
|
||||||
<string name="settings_playback_hold_to_speed">Przytrzymaj, aby przyspieszyć</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>
|
<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_option_none">Brak</string>
|
||||||
<string name="settings_playback_prefer_binge_group">Preferuj grupę binge</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_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_audio_language">Preferowany język audio</string>
|
||||||
<string name="settings_playback_preferred_subtitle_language">Preferowany język napisów</string>
|
<string name="settings_playback_preferred_subtitle_language">Preferowany język napisów</string>
|
||||||
<string name="settings_playback_presets">Szablony</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_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_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="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_audience_score">Ocena widzów</string>
|
||||||
<string name="source_imdb">IMDb</string>
|
<string name="source_imdb">IMDb</string>
|
||||||
<string name="source_letterboxd">Letterboxd</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="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_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_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">Wybierz awatar</string>
|
||||||
<string name="profile_choose_avatar_below">Wybierz awatar poniżej.</string>
|
<string name="profile_choose_avatar_below">Wybierz awatar poniżej.</string>
|
||||||
<string name="profile_create_profile">Utwórz profil</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_confirm_message">Wszystkie dane profilu „%1$s" zostaną trwale usunięte.</string>
|
||||||
<string name="profile_delete_title">Usuń profil</string>
|
<string name="profile_delete_title">Usuń profil</string>
|
||||||
<string name="profile_edit_add_title">Dodaj 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_checking_more_addons">Sprawdzanie kolejnych dodatków…</string>
|
||||||
<string name="streams_copy_link">Kopiuj link strumienia</string>
|
<string name="streams_copy_link">Kopiuj link strumienia</string>
|
||||||
<string name="streams_download_file">Pobierz plik</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_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_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>
|
<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_percent">Wznów od %1$d%</string>
|
||||||
<string name="streams_resume_from_time">Wznów od %1$s</string>
|
<string name="streams_resume_from_time">Wznów od %1$s</string>
|
||||||
<string name="streams_size">ROZMIAR %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_close">Zamknij zwiastun</string>
|
||||||
<string name="trailer_unable_to_play">Nie można odtworzyć zwiastuna</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>
|
<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_failed">Pobieranie nie powiodło się</string>
|
||||||
<string name="downloads_live_paused">Wstrzymano %1$s</string>
|
<string name="downloads_live_paused">Wstrzymano %1$s</string>
|
||||||
<string name="library_remove_confirm">Usuń</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_message">Usunąć %1$s z biblioteki?</string>
|
||||||
<string name="library_remove_title">Usunąć z biblioteki?</string>
|
<string name="library_remove_title">Usunąć z biblioteki?</string>
|
||||||
<string name="media_movie">Film</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_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_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_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_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="collections_folder_addon_not_found">Nie znaleziono dodatku: %1$s</string>
|
||||||
<string name="date_month_january">Styczeń</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_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_episode_release_body_title">%1$s jest już dostępny</string>
|
||||||
<string name="notifications_channel_episode_releases_name">Premiery odcinków</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_creator">Twórca</string>
|
||||||
<string name="person_role_director">Reżyser</string>
|
<string name="person_role_director">Reżyser</string>
|
||||||
<string name="person_role_writer">Scenarzysta</string>
|
<string name="person_role_writer">Scenarzysta</string>
|
||||||
|
|
|
||||||
|
|
@ -756,6 +756,8 @@
|
||||||
<string name="settings_playback_option_none">None</string>
|
<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">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_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_audio_language">Preferred Audio Language</string>
|
||||||
<string name="settings_playback_preferred_subtitle_language">Preferred Language</string>
|
<string name="settings_playback_preferred_subtitle_language">Preferred Language</string>
|
||||||
<string name="settings_playback_presets">Presets</string>
|
<string name="settings_playback_presets">Presets</string>
|
||||||
|
|
|
||||||
|
|
@ -1527,6 +1527,7 @@ private fun MainAppContent(
|
||||||
StreamsRepository.reload(
|
StreamsRepository.reload(
|
||||||
type = launch.type,
|
type = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
|
parentMetaId = launch.parentMetaId,
|
||||||
season = launch.seasonNumber,
|
season = launch.seasonNumber,
|
||||||
episode = launch.episodeNumber,
|
episode = launch.episodeNumber,
|
||||||
manualSelection = launch.manualSelection,
|
manualSelection = launch.manualSelection,
|
||||||
|
|
@ -1636,6 +1637,7 @@ private fun MainAppContent(
|
||||||
StreamsRepository.reload(
|
StreamsRepository.reload(
|
||||||
type = launch.type,
|
type = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
|
parentMetaId = launch.parentMetaId,
|
||||||
season = launch.seasonNumber,
|
season = launch.seasonNumber,
|
||||||
episode = launch.episodeNumber,
|
episode = launch.episodeNumber,
|
||||||
manualSelection = launch.manualSelection,
|
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.SkipIntroButton
|
||||||
import com.nuvio.app.features.player.skip.SkipIntroRepository
|
import com.nuvio.app.features.player.skip.SkipIntroRepository
|
||||||
import com.nuvio.app.features.player.skip.SkipInterval
|
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.StreamAutoPlayMode
|
||||||
import com.nuvio.app.features.streams.StreamAutoPlaySelector
|
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.StreamItem
|
||||||
import com.nuvio.app.features.streams.StreamLinkCacheRepository
|
import com.nuvio.app.features.streams.StreamLinkCacheRepository
|
||||||
import com.nuvio.app.features.streams.StreamsUiState
|
import com.nuvio.app.features.streams.StreamsUiState
|
||||||
|
|
@ -73,6 +75,7 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import nuvio.composeapp.generated.resources.*
|
import nuvio.composeapp.generated.resources.*
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
@ -86,6 +89,8 @@ private const val PlayerLockedOverlayDurationMs = 2_000L
|
||||||
private const val PlayerLeftGestureBoundary = 0.4f
|
private const val PlayerLeftGestureBoundary = 0.4f
|
||||||
private const val PlayerRightGestureBoundary = 0.6f
|
private const val PlayerRightGestureBoundary = 0.6f
|
||||||
private const val PlayerVerticalGestureSensitivity = 1f
|
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 PlayerSliderOverlayGap = 12.dp
|
||||||
private val PlayerTimeRowHeight = 36.dp
|
private val PlayerTimeRowHeight = 36.dp
|
||||||
private val PlayerActionRowHeight = 50.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(
|
ManagePlayerPictureInPicture(
|
||||||
isPlaying = playbackSnapshot.isPlaying,
|
isPlaying = playbackSnapshot.isPlaying,
|
||||||
playerSize = layoutSize,
|
playerSize = layoutSize,
|
||||||
|
|
@ -1101,6 +1115,12 @@ fun PlayerScreen(
|
||||||
settings.streamAutoPlayPreferBingeGroup
|
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
|
// Determine auto-play mode for next episode
|
||||||
val effectiveMode = if (shouldAutoSelectInManualMode) {
|
val effectiveMode = if (shouldAutoSelectInManualMode) {
|
||||||
StreamAutoPlayMode.FIRST_STREAM
|
StreamAutoPlayMode.FIRST_STREAM
|
||||||
|
|
@ -1108,7 +1128,7 @@ fun PlayerScreen(
|
||||||
settings.streamAutoPlayMode
|
settings.streamAutoPlayMode
|
||||||
}
|
}
|
||||||
val effectiveSource = if (shouldAutoSelectInManualMode) {
|
val effectiveSource = if (shouldAutoSelectInManualMode) {
|
||||||
com.nuvio.app.features.streams.StreamAutoPlaySource.ALL_SOURCES
|
StreamAutoPlaySource.ALL_SOURCES
|
||||||
} else {
|
} else {
|
||||||
settings.streamAutoPlaySource
|
settings.streamAutoPlaySource
|
||||||
}
|
}
|
||||||
|
|
@ -1128,6 +1148,13 @@ fun PlayerScreen(
|
||||||
settings.streamAutoPlayRegex
|
settings.streamAutoPlayRegex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine preferred binge group from current stream (not cache)
|
||||||
|
val preferredBingeGroup = if (settings.streamAutoPlayPreferBingeGroup) {
|
||||||
|
currentStreamBingeGroup
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
nextEpisodeAutoPlayJob = scope.launch {
|
nextEpisodeAutoPlayJob = scope.launch {
|
||||||
PlayerStreamsRepository.loadEpisodeStreams(
|
PlayerStreamsRepository.loadEpisodeStreams(
|
||||||
type = type,
|
type = type,
|
||||||
|
|
@ -1140,59 +1167,171 @@ fun PlayerScreen(
|
||||||
.map { it.displayTitle }
|
.map { it.displayTitle }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
val timeoutMs = settings.streamAutoPlayTimeoutSeconds * 1000L
|
val timeoutSeconds = settings.streamAutoPlayTimeoutSeconds
|
||||||
val startTime = WatchProgressClock.nowEpochMs()
|
val isUnlimitedTimeout = timeoutSeconds == Int.MAX_VALUE
|
||||||
|
var autoSelectTriggered = false
|
||||||
|
var timeoutElapsed = false
|
||||||
|
var selectedStream: StreamItem? = null
|
||||||
|
|
||||||
// Collect streams as they arrive
|
// Full select: tries binge group first, then falls back to mode-based selection
|
||||||
PlayerStreamsRepository.episodeStreamsState.collectLatest { state ->
|
fun trySelectStream(streams: List<StreamItem>): StreamItem? {
|
||||||
if (state.groups.isEmpty() && state.isAnyLoading) return@collectLatest
|
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 }
|
// Binge group only early match: returns null if no binge group match
|
||||||
val elapsed = WatchProgressClock.nowEpochMs() - startTime
|
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()) {
|
val innerJob = launch {
|
||||||
StreamAutoPlaySelector.selectAutoPlayStream(
|
// Collect streams as they arrive
|
||||||
streams = allStreams,
|
PlayerStreamsRepository.episodeStreamsState.collectLatest { state ->
|
||||||
mode = effectiveMode,
|
if (state.groups.isEmpty() && state.isAnyLoading) return@collectLatest
|
||||||
regexPattern = effectiveRegex,
|
|
||||||
source = effectiveSource,
|
|
||||||
installedAddonNames = installedAddonNames,
|
|
||||||
selectedAddons = effectiveSelectedAddons,
|
|
||||||
selectedPlugins = effectiveSelectedPlugins,
|
|
||||||
preferredBingeGroup = if (settings.streamAutoPlayPreferBingeGroup) {
|
|
||||||
currentStreamBingeGroup
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
preferBingeGroupInSelection = settings.streamAutoPlayPreferBingeGroup,
|
|
||||||
)
|
|
||||||
} else null
|
|
||||||
|
|
||||||
if (selected != null || !state.isAnyLoading || elapsed >= timeoutMs) {
|
val allStreams = state.groups.flatMap { it.streams }
|
||||||
nextEpisodeAutoPlaySearching = false
|
|
||||||
if (selected != null) {
|
if (autoSelectTriggered) {
|
||||||
nextEpisodeAutoPlaySourceName = selected.addonName
|
// Already resolved
|
||||||
// Countdown before playing
|
} else if (timeoutElapsed) {
|
||||||
for (i in 3 downTo 1) {
|
// Timeout elapsed: full select (binge group + fallback to mode)
|
||||||
nextEpisodeAutoPlayCountdown = i
|
if (allStreams.isNotEmpty()) {
|
||||||
delay(1000)
|
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
|
// Skip intro/recap/outro button
|
||||||
if (!playerControlsLocked) {
|
if (!playerControlsLocked) {
|
||||||
SkipIntroButton(
|
SkipIntroButton(
|
||||||
interval = activeSkipInterval,
|
interval = if (!initialLoadCompleted || pausedOverlayVisible) null else activeSkipInterval,
|
||||||
dismissed = skipIntervalDismissed,
|
dismissed = skipIntervalDismissed,
|
||||||
controlsVisible = controlsVisible,
|
controlsVisible = controlsVisible,
|
||||||
onSkip = {
|
onSkip = {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,29 @@ import com.nuvio.app.features.streams.StreamAutoPlaySource
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
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(
|
data class PlayerSettingsUiState(
|
||||||
val showLoadingOverlay: Boolean = true,
|
val showLoadingOverlay: Boolean = true,
|
||||||
|
|
@ -38,6 +61,7 @@ data class PlayerSettingsUiState(
|
||||||
val introSubmitEnabled: Boolean = false,
|
val introSubmitEnabled: Boolean = false,
|
||||||
val streamAutoPlayNextEpisodeEnabled: Boolean = false,
|
val streamAutoPlayNextEpisodeEnabled: Boolean = false,
|
||||||
val streamAutoPlayPreferBingeGroup: Boolean = true,
|
val streamAutoPlayPreferBingeGroup: Boolean = true,
|
||||||
|
val streamAutoPlayReuseBingeGroup: Boolean = true,
|
||||||
val nextEpisodeThresholdMode: NextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE,
|
val nextEpisodeThresholdMode: NextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE,
|
||||||
val nextEpisodeThresholdPercent: Float = 99f,
|
val nextEpisodeThresholdPercent: Float = 99f,
|
||||||
val nextEpisodeThresholdMinutesBeforeEnd: Float = 2f,
|
val nextEpisodeThresholdMinutesBeforeEnd: Float = 2f,
|
||||||
|
|
@ -93,6 +117,7 @@ object PlayerSettingsRepository {
|
||||||
private var introSubmitEnabled = false
|
private var introSubmitEnabled = false
|
||||||
private var streamAutoPlayNextEpisodeEnabled = false
|
private var streamAutoPlayNextEpisodeEnabled = false
|
||||||
private var streamAutoPlayPreferBingeGroup = true
|
private var streamAutoPlayPreferBingeGroup = true
|
||||||
|
private var streamAutoPlayReuseBingeGroup = true
|
||||||
private var nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE
|
private var nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE
|
||||||
private var nextEpisodeThresholdPercent = 99f
|
private var nextEpisodeThresholdPercent = 99f
|
||||||
private var nextEpisodeThresholdMinutesBeforeEnd = 2f
|
private var nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||||
|
|
@ -153,6 +178,7 @@ object PlayerSettingsRepository {
|
||||||
introSubmitEnabled = false
|
introSubmitEnabled = false
|
||||||
streamAutoPlayNextEpisodeEnabled = false
|
streamAutoPlayNextEpisodeEnabled = false
|
||||||
streamAutoPlayPreferBingeGroup = true
|
streamAutoPlayPreferBingeGroup = true
|
||||||
|
streamAutoPlayReuseBingeGroup = true
|
||||||
nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE
|
nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE
|
||||||
nextEpisodeThresholdPercent = 99f
|
nextEpisodeThresholdPercent = 99f
|
||||||
nextEpisodeThresholdMinutesBeforeEnd = 2f
|
nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||||
|
|
@ -232,6 +258,14 @@ object PlayerSettingsRepository {
|
||||||
}
|
}
|
||||||
streamAutoPlayRegex = PlayerSettingsStorage.loadStreamAutoPlayRegex() ?: ""
|
streamAutoPlayRegex = PlayerSettingsStorage.loadStreamAutoPlayRegex() ?: ""
|
||||||
streamAutoPlayTimeoutSeconds = PlayerSettingsStorage.loadStreamAutoPlayTimeoutSeconds() ?: 3
|
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
|
skipIntroEnabled = PlayerSettingsStorage.loadSkipIntroEnabled() ?: true
|
||||||
animeSkipEnabled = PlayerSettingsStorage.loadAnimeSkipEnabled() ?: false
|
animeSkipEnabled = PlayerSettingsStorage.loadAnimeSkipEnabled() ?: false
|
||||||
animeSkipClientId = PlayerSettingsStorage.loadAnimeSkipClientId() ?: ""
|
animeSkipClientId = PlayerSettingsStorage.loadAnimeSkipClientId() ?: ""
|
||||||
|
|
@ -239,6 +273,7 @@ object PlayerSettingsRepository {
|
||||||
introSubmitEnabled = PlayerSettingsStorage.loadIntroSubmitEnabled() ?: false
|
introSubmitEnabled = PlayerSettingsStorage.loadIntroSubmitEnabled() ?: false
|
||||||
streamAutoPlayNextEpisodeEnabled = PlayerSettingsStorage.loadStreamAutoPlayNextEpisodeEnabled() ?: false
|
streamAutoPlayNextEpisodeEnabled = PlayerSettingsStorage.loadStreamAutoPlayNextEpisodeEnabled() ?: false
|
||||||
streamAutoPlayPreferBingeGroup = PlayerSettingsStorage.loadStreamAutoPlayPreferBingeGroup() ?: true
|
streamAutoPlayPreferBingeGroup = PlayerSettingsStorage.loadStreamAutoPlayPreferBingeGroup() ?: true
|
||||||
|
streamAutoPlayReuseBingeGroup = PlayerSettingsStorage.loadStreamAutoPlayReuseBingeGroup() ?: true
|
||||||
nextEpisodeThresholdMode = PlayerSettingsStorage.loadNextEpisodeThresholdMode()
|
nextEpisodeThresholdMode = PlayerSettingsStorage.loadNextEpisodeThresholdMode()
|
||||||
?.let { runCatching { NextEpisodeThresholdMode.valueOf(it) }.getOrNull() }
|
?.let { runCatching { NextEpisodeThresholdMode.valueOf(it) }.getOrNull() }
|
||||||
?: NextEpisodeThresholdMode.PERCENTAGE
|
?: NextEpisodeThresholdMode.PERCENTAGE
|
||||||
|
|
@ -524,6 +559,14 @@ object PlayerSettingsRepository {
|
||||||
PlayerSettingsStorage.saveStreamAutoPlayPreferBingeGroup(enabled)
|
PlayerSettingsStorage.saveStreamAutoPlayPreferBingeGroup(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setStreamAutoPlayReuseBingeGroup(enabled: Boolean) {
|
||||||
|
ensureLoaded()
|
||||||
|
if (streamAutoPlayReuseBingeGroup == enabled) return
|
||||||
|
streamAutoPlayReuseBingeGroup = enabled
|
||||||
|
publish()
|
||||||
|
PlayerSettingsStorage.saveStreamAutoPlayReuseBingeGroup(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
fun setNextEpisodeThresholdMode(mode: NextEpisodeThresholdMode) {
|
fun setNextEpisodeThresholdMode(mode: NextEpisodeThresholdMode) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
if (nextEpisodeThresholdMode == mode) return
|
if (nextEpisodeThresholdMode == mode) return
|
||||||
|
|
@ -753,6 +796,7 @@ object PlayerSettingsRepository {
|
||||||
introSubmitEnabled = introSubmitEnabled,
|
introSubmitEnabled = introSubmitEnabled,
|
||||||
streamAutoPlayNextEpisodeEnabled = streamAutoPlayNextEpisodeEnabled,
|
streamAutoPlayNextEpisodeEnabled = streamAutoPlayNextEpisodeEnabled,
|
||||||
streamAutoPlayPreferBingeGroup = streamAutoPlayPreferBingeGroup,
|
streamAutoPlayPreferBingeGroup = streamAutoPlayPreferBingeGroup,
|
||||||
|
streamAutoPlayReuseBingeGroup = streamAutoPlayReuseBingeGroup,
|
||||||
nextEpisodeThresholdMode = nextEpisodeThresholdMode,
|
nextEpisodeThresholdMode = nextEpisodeThresholdMode,
|
||||||
nextEpisodeThresholdPercent = nextEpisodeThresholdPercent,
|
nextEpisodeThresholdPercent = nextEpisodeThresholdPercent,
|
||||||
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
|
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ internal expect object PlayerSettingsStorage {
|
||||||
fun saveStreamAutoPlayNextEpisodeEnabled(enabled: Boolean)
|
fun saveStreamAutoPlayNextEpisodeEnabled(enabled: Boolean)
|
||||||
fun loadStreamAutoPlayPreferBingeGroup(): Boolean?
|
fun loadStreamAutoPlayPreferBingeGroup(): Boolean?
|
||||||
fun saveStreamAutoPlayPreferBingeGroup(enabled: Boolean)
|
fun saveStreamAutoPlayPreferBingeGroup(enabled: Boolean)
|
||||||
|
fun loadStreamAutoPlayReuseBingeGroup(): Boolean?
|
||||||
|
fun saveStreamAutoPlayReuseBingeGroup(enabled: Boolean)
|
||||||
fun loadNextEpisodeThresholdMode(): String?
|
fun loadNextEpisodeThresholdMode(): String?
|
||||||
fun saveNextEpisodeThresholdMode(mode: String)
|
fun saveNextEpisodeThresholdMode(mode: String)
|
||||||
fun loadNextEpisodeThresholdPercent(): Float?
|
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.IosTargetPrimaries
|
||||||
import com.nuvio.app.features.player.IosTargetTransfer
|
import com.nuvio.app.features.player.IosTargetTransfer
|
||||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
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.SubtitleLanguageOption
|
||||||
import com.nuvio.app.features.player.formatPlaybackSpeedLabel
|
import com.nuvio.app.features.player.formatPlaybackSpeedLabel
|
||||||
import com.nuvio.app.features.player.languageLabelForCode
|
import com.nuvio.app.features.player.languageLabelForCode
|
||||||
|
|
@ -365,7 +366,7 @@ private fun PlaybackSettingsSection(
|
||||||
val timeoutSec = autoPlayPlayerSettings.streamAutoPlayTimeoutSeconds
|
val timeoutSec = autoPlayPlayerSettings.streamAutoPlayTimeoutSeconds
|
||||||
val timeoutLabel = when (timeoutSec) {
|
val timeoutLabel = when (timeoutSec) {
|
||||||
0 -> stringResource(Res.string.settings_playback_timeout_instant)
|
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)
|
else -> stringResource(Res.string.settings_playback_timeout_seconds, timeoutSec)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -391,8 +392,11 @@ private fun PlaybackSettingsSection(
|
||||||
}
|
}
|
||||||
ValueBox(text = timeoutLabel, modifier = Modifier.wrapContentWidth())
|
ValueBox(text = timeoutLabel, modifier = Modifier.wrapContentWidth())
|
||||||
}
|
}
|
||||||
var sliderValue by remember(timeoutSec) { mutableFloatStateOf(timeoutSec.toFloat()) }
|
val timeoutIndex = STREAM_AUTO_PLAY_TIMEOUT_VALUES.indexOf(timeoutSec)
|
||||||
var lastHapticStep by remember(timeoutSec) { mutableStateOf(timeoutSec.toFloat()) }
|
.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(
|
Slider(
|
||||||
value = sliderValue,
|
value = sliderValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
|
|
@ -405,10 +409,11 @@ private fun PlaybackSettingsSection(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onValueChangeFinished = {
|
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,
|
valueRange = 0f..maxIndex,
|
||||||
steps = calculateSteps(0f, 11f, 1f),
|
steps = calculateSteps(0f, maxIndex, 1f),
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = MaterialTheme.colorScheme.primary,
|
thumbColor = MaterialTheme.colorScheme.primary,
|
||||||
activeTrackColor = MaterialTheme.colorScheme.primary,
|
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
|
@ -658,6 +663,16 @@ private fun PlaybackSettingsSection(
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
onCheckedChange = PlayerSettingsRepository::setStreamAutoPlayPreferBingeGroup,
|
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)
|
SettingsGroupDivider(isTablet = isTablet)
|
||||||
var showThresholdModeDialog by remember { mutableStateOf(false) }
|
var showThresholdModeDialog by remember { mutableStateOf(false) }
|
||||||
SettingsNavigationRow(
|
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 {
|
object StreamAutoPlayPolicy {
|
||||||
fun isEffectivelyEnabled(settings: PlayerSettingsUiState): Boolean {
|
fun isEffectivelyEnabled(settings: PlayerSettingsUiState): Boolean {
|
||||||
if (settings.streamReuseLastLinkEnabled) return true
|
if (settings.streamReuseLastLinkEnabled) return true
|
||||||
|
if (settings.streamAutoPlayReuseBingeGroup && settings.streamAutoPlayPreferBingeGroup) return true
|
||||||
|
|
||||||
return when (settings.streamAutoPlayMode) {
|
return when (settings.streamAutoPlayMode) {
|
||||||
StreamAutoPlayMode.MANUAL -> false
|
StreamAutoPlayMode.MANUAL -> false
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ object StreamAutoPlaySelector {
|
||||||
selectedPlugins: Set<String>,
|
selectedPlugins: Set<String>,
|
||||||
preferredBingeGroup: String? = null,
|
preferredBingeGroup: String? = null,
|
||||||
preferBingeGroupInSelection: Boolean = false,
|
preferBingeGroupInSelection: Boolean = false,
|
||||||
|
bingeGroupOnly: Boolean = false,
|
||||||
): StreamItem? {
|
): StreamItem? {
|
||||||
if (streams.isEmpty()) return null
|
if (streams.isEmpty()) return null
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ object StreamAutoPlaySelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (candidateStreams.isEmpty()) return null
|
if (candidateStreams.isEmpty()) return null
|
||||||
if (mode == StreamAutoPlayMode.MANUAL) return null
|
if (mode == StreamAutoPlayMode.MANUAL && !bingeGroupOnly) return null
|
||||||
|
|
||||||
val targetBingeGroup = preferredBingeGroup?.trim().orEmpty()
|
val targetBingeGroup = preferredBingeGroup?.trim().orEmpty()
|
||||||
if (preferBingeGroupInSelection && targetBingeGroup.isNotEmpty()) {
|
if (preferBingeGroupInSelection && targetBingeGroup.isNotEmpty()) {
|
||||||
|
|
@ -65,6 +66,12 @@ object StreamAutoPlaySelector {
|
||||||
stream.behaviorHints.bingeGroup == targetBingeGroup && stream.isAutoPlayable()
|
stream.behaviorHints.bingeGroup == targetBingeGroup && stream.isAutoPlayable()
|
||||||
}
|
}
|
||||||
if (bingeGroupMatch != null) return bingeGroupMatch
|
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) {
|
return when (mode) {
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,11 @@ object StreamsRepository {
|
||||||
): String =
|
): String =
|
||||||
"$type::$videoId::$season::$episode::$manualSelection"
|
"$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(
|
load(
|
||||||
type = type,
|
type = type,
|
||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
|
parentMetaId = parentMetaId,
|
||||||
season = season,
|
season = season,
|
||||||
episode = episode,
|
episode = episode,
|
||||||
manualSelection = manualSelection,
|
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(
|
load(
|
||||||
type = type,
|
type = type,
|
||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
|
parentMetaId = parentMetaId,
|
||||||
season = season,
|
season = season,
|
||||||
episode = episode,
|
episode = episode,
|
||||||
manualSelection = manualSelection,
|
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) {
|
val pluginUiState = if (AppFeaturePolicy.pluginsEnabled) {
|
||||||
PluginRepository.initialize()
|
PluginRepository.initialize()
|
||||||
PluginRepository.uiState.value
|
PluginRepository.uiState.value
|
||||||
|
|
@ -105,7 +107,21 @@ object StreamsRepository {
|
||||||
val isAutoPlayEnabled = !manualSelection && autoPlayMode != StreamAutoPlayMode.MANUAL &&
|
val isAutoPlayEnabled = !manualSelection && autoPlayMode != StreamAutoPlayMode.MANUAL &&
|
||||||
!(autoPlayMode == StreamAutoPlayMode.REGEX_MATCH &&
|
!(autoPlayMode == StreamAutoPlayMode.REGEX_MATCH &&
|
||||||
!StreamAutoPlayPolicy.isRegexSelectionConfigured(playerSettings.streamAutoPlayRegex))
|
!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) {
|
if (isDirectAutoPlayFlow) {
|
||||||
_uiState.value = StreamsUiState(
|
_uiState.value = StreamsUiState(
|
||||||
|
|
@ -237,16 +253,59 @@ object StreamsRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val timeoutJob = if (isAutoPlayEnabled) {
|
val timeoutJob = if (isDirectAutoPlayFlow) {
|
||||||
val timeoutMs = playerSettings.streamAutoPlayTimeoutSeconds * 1_000L
|
val timeoutSeconds = playerSettings.streamAutoPlayTimeoutSeconds
|
||||||
if (timeoutMs > 0L && playerSettings.streamAutoPlayTimeoutSeconds < 11) {
|
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 {
|
launch {
|
||||||
delay(timeoutMs)
|
delay(timeoutSeconds * 1_000L)
|
||||||
timeoutElapsed = true
|
timeoutElapsed = true
|
||||||
if (!autoSelectTriggered) {
|
if (!autoSelectTriggered) {
|
||||||
val allStreams = _uiState.value.groups.flatMap { it.streams }
|
val allStreams = _uiState.value.groups.flatMap { it.streams }
|
||||||
if (allStreams.isNotEmpty()) {
|
if (allStreams.isNotEmpty()) {
|
||||||
autoSelectTriggered = true
|
|
||||||
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
|
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
|
||||||
streams = allStreams,
|
streams = allStreams,
|
||||||
mode = autoPlayMode,
|
mode = autoPlayMode,
|
||||||
|
|
@ -255,9 +314,14 @@ object StreamsRepository {
|
||||||
installedAddonNames = installedAddonNames,
|
installedAddonNames = installedAddonNames,
|
||||||
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
||||||
selectedPlugins = playerSettings.streamAutoPlaySelectedPlugins,
|
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 {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isDirectAutoPlayFlow = false,
|
isDirectAutoPlayFlow = false,
|
||||||
|
|
@ -268,11 +332,6 @@ object StreamsRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (timeoutMs <= 0L) {
|
|
||||||
timeoutElapsed = true
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
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
|
autoSelectTriggered = true
|
||||||
val allStreams = _uiState.value.groups.flatMap { it.streams }
|
val allStreams = _uiState.value.groups.flatMap { it.streams }
|
||||||
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
|
val selected = StreamAutoPlaySelector.selectAutoPlayStream(
|
||||||
|
|
@ -492,6 +596,9 @@ object StreamsRepository {
|
||||||
installedAddonNames = installedAddonNames,
|
installedAddonNames = installedAddonNames,
|
||||||
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
selectedAddons = playerSettings.streamAutoPlaySelectedAddons,
|
||||||
selectedPlugins = playerSettings.streamAutoPlaySelectedPlugins,
|
selectedPlugins = playerSettings.streamAutoPlaySelectedPlugins,
|
||||||
|
preferredBingeGroup = persistedBingeGroup,
|
||||||
|
preferBingeGroupInSelection = persistedBingeGroup != null,
|
||||||
|
bingeGroupOnly = false,
|
||||||
)
|
)
|
||||||
_uiState.update { it.copy(autoPlayStream = selected) }
|
_uiState.update { it.copy(autoPlayStream = selected) }
|
||||||
}
|
}
|
||||||
|
|
@ -512,6 +619,7 @@ object StreamsRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consumeAutoPlay() {
|
fun consumeAutoPlay() {
|
||||||
|
activeRequestKey = null
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
autoPlayStream = null,
|
autoPlayStream = null,
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ fun StreamsScreen(
|
||||||
StreamsRepository.load(
|
StreamsRepository.load(
|
||||||
type = type,
|
type = type,
|
||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
|
parentMetaId = parentMetaId,
|
||||||
season = seasonNumber,
|
season = seasonNumber,
|
||||||
episode = episodeNumber,
|
episode = episodeNumber,
|
||||||
manualSelection = manualSelection,
|
manualSelection = manualSelection,
|
||||||
|
|
@ -277,6 +278,7 @@ fun StreamsScreen(
|
||||||
StreamsRepository.reload(
|
StreamsRepository.reload(
|
||||||
type = type,
|
type = type,
|
||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
|
parentMetaId = parentMetaId,
|
||||||
season = seasonNumber,
|
season = seasonNumber,
|
||||||
episode = episodeNumber,
|
episode = episodeNumber,
|
||||||
manualSelection = manualSelection,
|
manualSelection = manualSelection,
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ actual object PlayerSettingsStorage {
|
||||||
private const val introSubmitEnabledKey = "intro_submit_enabled"
|
private const val introSubmitEnabledKey = "intro_submit_enabled"
|
||||||
private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled"
|
private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled"
|
||||||
private const val streamAutoPlayPreferBingeGroupKey = "stream_auto_play_prefer_binge_group"
|
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 nextEpisodeThresholdModeKey = "next_episode_threshold_mode"
|
||||||
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
|
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
|
||||||
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||||
|
|
@ -99,6 +100,7 @@ actual object PlayerSettingsStorage {
|
||||||
animeSkipClientIdKey,
|
animeSkipClientIdKey,
|
||||||
streamAutoPlayNextEpisodeEnabledKey,
|
streamAutoPlayNextEpisodeEnabledKey,
|
||||||
streamAutoPlayPreferBingeGroupKey,
|
streamAutoPlayPreferBingeGroupKey,
|
||||||
|
streamAutoPlayReuseBingeGroupKey,
|
||||||
nextEpisodeThresholdModeKey,
|
nextEpisodeThresholdModeKey,
|
||||||
nextEpisodeThresholdPercentKey,
|
nextEpisodeThresholdPercentKey,
|
||||||
nextEpisodeThresholdMinutesBeforeEndKey,
|
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||||
|
|
@ -554,6 +556,20 @@ actual object PlayerSettingsStorage {
|
||||||
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(streamAutoPlayPreferBingeGroupKey))
|
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? {
|
actual fun loadNextEpisodeThresholdMode(): String? {
|
||||||
val defaults = NSUserDefaults.standardUserDefaults
|
val defaults = NSUserDefaults.standardUserDefaults
|
||||||
val key = ProfileScopedKey.of(nextEpisodeThresholdModeKey)
|
val key = ProfileScopedKey.of(nextEpisodeThresholdModeKey)
|
||||||
|
|
@ -725,6 +741,7 @@ actual object PlayerSettingsStorage {
|
||||||
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
|
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
|
||||||
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
|
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
|
||||||
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
|
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
|
||||||
|
loadStreamAutoPlayReuseBingeGroup()?.let { put(streamAutoPlayReuseBingeGroupKey, encodeSyncBoolean(it)) }
|
||||||
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
|
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
|
||||||
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
|
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
|
||||||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||||
|
|
@ -782,6 +799,7 @@ actual object PlayerSettingsStorage {
|
||||||
payload.decodeSyncString(introDbApiKeyKey)?.let(::saveIntroDbApiKey)
|
payload.decodeSyncString(introDbApiKeyKey)?.let(::saveIntroDbApiKey)
|
||||||
payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
|
payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
|
||||||
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
|
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
|
||||||
|
payload.decodeSyncBoolean(streamAutoPlayReuseBingeGroupKey)?.let(::saveStreamAutoPlayReuseBingeGroup)
|
||||||
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
|
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
|
||||||
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
|
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
|
||||||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
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