Merge branch 'NuvioMedia:cmp-rewrite' into cmp-rewrite

This commit is contained in:
skoruppa 2026-04-30 10:43:20 +02:00 committed by GitHub
commit 7a0cc2c03b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 2742 additions and 130 deletions

View file

@ -272,7 +272,7 @@ kotlin {
afterEvaluate {
dependencies {
add("fullImplementation", libs.quickjs.kt)
add("fullImplementation", files("libs/quickjs-kt-android-1.0.5-nuvio.aar"))
add("fullImplementation", libs.ksoup)
}
}

Binary file not shown.

View file

@ -18,7 +18,8 @@ actual object ThemeSettingsStorage {
private const val selectedThemeKey = "selected_theme"
private const val amoledEnabledKey = "amoled_enabled"
private const val selectedAppLanguageKey = "selected_app_language"
private val syncKeys = listOf(selectedThemeKey, amoledEnabledKey, selectedAppLanguageKey)
private val profileScopedSyncKeys = listOf(selectedThemeKey, amoledEnabledKey)
private val globalSyncKeys = listOf(selectedAppLanguageKey)
private var preferences: SharedPreferences? = null
@ -50,13 +51,18 @@ actual object ThemeSettingsStorage {
?.apply()
}
actual fun loadSelectedAppLanguage(): String? =
preferences?.getString(ProfileScopedKey.of(selectedAppLanguageKey), null)
actual fun loadSelectedAppLanguage(): String? {
val value = preferences?.getString(selectedAppLanguageKey, null)
if (value != null) return value
val legacy = preferences?.getString(ProfileScopedKey.of(selectedAppLanguageKey), null)
if (legacy != null) saveSelectedAppLanguage(legacy)
return legacy
}
actual fun saveSelectedAppLanguage(languageCode: String) {
preferences
?.edit()
?.putString(ProfileScopedKey.of(selectedAppLanguageKey), languageCode)
?.putString(selectedAppLanguageKey, languageCode)
?.apply()
}
@ -74,7 +80,8 @@ actual object ThemeSettingsStorage {
actual fun replaceFromSyncPayload(payload: JsonObject) {
preferences?.edit()?.apply {
syncKeys.forEach { remove(ProfileScopedKey.of(it)) }
profileScopedSyncKeys.forEach { remove(ProfileScopedKey.of(it)) }
globalSyncKeys.forEach { remove(it) }
}?.apply()
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)

View file

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en"/>
<locale android:name="fr"/>
<locale android:name="es"/>
<locale android:name="pt-PT"/>
<locale android:name="tr"/>
<locale android:name="it"/>
<locale android:name="el"/>

View file

@ -569,7 +569,7 @@
<string name="settings_playback_threshold_mode_percentage">Ποσοστό</string>
<string name="settings_playback_threshold_percentage">Ποσοστό κατωφλίου</string>
<string name="settings_playback_threshold_percentage_description">Εμφάνιση κάρτας επόμενου επεισοδίου όταν η αναπαραγωγή φτάσει σε αυτό το ποσοστό.</string>
<string name="settings_playback_threshold_percentage_value">%1$d%%</string>
<string name="settings_playback_threshold_percentage_value">%1$d%</string>
<string name="settings_playback_timeout_instant">Άμεσα</string>
<string name="settings_playback_timeout_seconds">%1$ds</string>
<string name="settings_playback_timeout_unlimited">Απεριόριστο</string>
@ -774,7 +774,7 @@
<string name="episode_mark_unwatched">Σήμανση ως μη παρακολουθηθέν</string>
<string name="episode_mark_watched">Σήμανση ως παρακολουθηθέν</string>
<string name="home_continue_watching_up_next">Επόμενο</string>
<string name="home_continue_watching_watched">%1$d%% παρακολουθήθηκε</string>
<string name="home_continue_watching_watched">%1$s παρακολουθήθηκε</string>
<string name="home_empty_no_active_addons_message">Εγκαταστήστε και επικυρώστε τουλάχιστον ένα πρόσθετο πριν φορτώσετε γραμμές καταλόγου στην Αρχική.</string>
<string name="home_empty_no_rows_message">Τα εγκατεστημένα πρόσθετα δεν παρέχουν αυτή τη στιγμή καταλόγους συμβατούς με πίνακα χωρίς απαιτούμενα extras.</string>
<string name="home_empty_no_rows_title">Δεν υπάρχουν διαθέσιμες γραμμές αρχικής</string>
@ -866,7 +866,7 @@
<string name="streams_no_direct_link">Δεν υπάρχει διαθέσιμος άμεσος σύνδεσμος ροής</string>
<string name="streams_no_metadata">Δεν υπάρχουν διαθέσιμα μεταδεδομένα</string>
<string name="streams_refresh">Ανανέωση ροών</string>
<string name="streams_resume_from_percent">Συνέχεια από %1$d%%</string>
<string name="streams_resume_from_percent">Συνέχεια από %1$d%</string>
<string name="streams_resume_from_time">Συνέχεια από %1$s</string>
<string name="streams_size">ΜΕΓΕΘΟΣ %1$s</string>
<string name="trailer_close">Κλείσιμο τρέιλερ</string>
@ -876,7 +876,7 @@
<string name="updates_asset_line">%1$s • %2$s</string>
<string name="updates_check_failed">Ο έλεγχος ενημερώσεων απέτυχε</string>
<string name="updates_download_failed">Η λήψη απέτυχε</string>
<string name="updates_downloading_progress">Λήψη %1$d%%</string>
<string name="updates_downloading_progress">Λήψη %1$d%</string>
<string name="updates_install_failed">Αδυναμία εκκίνησης εγκατάστασης</string>
<string name="updates_latest_version">Χρησιμοποιείτε την τελευταία έκδοση.</string>
<string name="updates_message_allow_installs">Ενεργοποιήστε τις εγκαταστάσεις εφαρμογών για το Nuvio και επιστρέψτε για να συνεχίσετε.</string>

View file

@ -219,7 +219,7 @@
<string name="collections_editor_tmdb_subtitle_movie_collection">Colección de películas de TMDB</string>
<string name="collections_editor_tmdb_subtitle_production">Producción</string>
<string name="collections_editor_tmdb_subtitle_network">Cadena</string>
<string name="collections_editor_tmdb_subtitle_discover">Discover de TMDB</string>
<string name="collections_editor_tmdb_subtitle_discover">Descubrir de TMDB</string>
<string name="collections_empty_subtitle">Crea una para organizar tus catálogos.</string>
<string name="collections_empty_title">Aún no hay colecciones</string>
<string name="collections_folder_count">%1$d carpeta(s)</string>
@ -298,7 +298,7 @@
<string name="compose_player_seek_forward_10">Avanzar 10 segundos</string>
<string name="compose_player_sources">Fuentes</string>
<string name="compose_player_style">Estilo</string>
<string name="compose_player_subs">Subs</string>
<string name="compose_player_subs">Subtítulos</string>
<string name="compose_player_subtitles">Subtítulos</string>
<string name="compose_player_brightness_level">Brillo %1$s</string>
<string name="compose_player_volume_level">Volumen %1$s</string>
@ -424,12 +424,12 @@
<string name="settings_homescreen_display_name">Nombre visible</string>
<string name="settings_homescreen_empty_message">Instala un complemento con catálogos compatibles con tableros para configurar las filas de la pantalla de inicio.</string>
<string name="settings_homescreen_empty_title">No hay catálogos de inicio</string>
<string name="settings_homescreen_hero_source">Fuente del Hero</string>
<string name="settings_homescreen_hero_source">Fuente de Destacado</string>
<string name="settings_homescreen_hidden">Oculto</string>
<string name="settings_homescreen_keep_home_focused">Mantener Inicio enfocado</string>
<string name="settings_homescreen_limit_reached">%1$s • Límite alcanzado (máx. %2$d)</string>
<string name="settings_homescreen_no_sources_selected">No hay fuentes Hero seleccionadas</string>
<string name="settings_homescreen_not_in_hero">No está en Hero</string>
<string name="settings_homescreen_no_sources_selected">No hay fuentes de Destacado seleccionadas</string>
<string name="settings_homescreen_not_in_hero">No está en Destacado</string>
<string name="settings_homescreen_pin_to_move_toast">Quita fijar arriba de la colección para moverla</string>
<string name="settings_homescreen_pinned">Fijado</string>
<string name="settings_homescreen_pinned_to_top">Fijado arriba</string>
@ -437,12 +437,12 @@
<string name="settings_homescreen_section_catalogs">CATÁLOGOS</string>
<string name="settings_homescreen_section_catalogs_collections">CATÁLOGOS Y COLECCIONES</string>
<string name="settings_homescreen_section_collections">COLECCIONES</string>
<string name="settings_homescreen_section_hero">HERO</string>
<string name="settings_homescreen_section_hero_sources">FUENTES DEL HERO</string>
<string name="settings_homescreen_section_hero">DESTACADO</string>
<string name="settings_homescreen_section_hero_sources">FUENTES DE DESTACADO</string>
<string name="settings_homescreen_selected_count">%1$d de %2$d seleccionados</string>
<string name="settings_homescreen_show_hero">Mostrar Hero</string>
<string name="settings_homescreen_show_hero_description">Mostrar un carrusel Hero destacado en la parte superior del inicio. Elige hasta 2 catálogos fuente abajo.</string>
<string name="settings_homescreen_summary">%1$d de %2$d catálogos visibles • %3$d fuentes Hero seleccionadas</string>
<string name="settings_homescreen_show_hero">Mostrar Destacado</string>
<string name="settings_homescreen_show_hero_description">Mostrar un carrusel destacado en la parte superior del inicio. Elige hasta 2 catálogos de origen abajo.</string>
<string name="settings_homescreen_summary">%1$d de %2$d catálogos visibles • %3$d fuentes de Destacado seleccionadas</string>
<string name="settings_homescreen_summary_hint">Abre un catálogo solo cuando necesites cambiarle el nombre o reordenarlo.</string>
<string name="settings_homescreen_visible">Visible</string>
<string name="settings_playback_subtitle">Reproductor, subtítulos y reproducción automática</string>
@ -488,7 +488,7 @@
<string name="settings_content_discovery_addons_description">Instala, elimina, actualiza y ordena tus fuentes de contenido.</string>
<string name="settings_content_discovery_plugins_description">Instala repositorios de scrapers en JavaScript y prueba proveedores internamente.</string>
<string name="settings_content_discovery_homescreen_description">Controla qué catálogos aparecen en Inicio y en qué orden.</string>
<string name="settings_content_discovery_meta_screen_description">Desactiva secciones de detalles y reordena todo debajo del Hero.</string>
<string name="settings_content_discovery_meta_screen_description">Desactiva secciones de detalles y reordena todo debajo del Destacado.</string>
<string name="settings_content_discovery_collections_description">Crea agrupaciones de catálogos personalizadas con carpetas mostradas en Inicio.</string>
<string name="settings_integrations_section_title">INTEGRACIONES</string>
<string name="settings_integrations_tmdb_description">Mejora las páginas de detalles con arte, créditos, metadatos de episodios y más desde TMDB.</string>
@ -641,7 +641,7 @@
<string name="settings_playback_regex_preset_quality_720p_smaller">720p / más pequeño</string>
<string name="settings_playback_regex_preset_web_sources">Fuentes WEB</string>
<string name="settings_playback_render_type">Tipo de renderizado</string>
<string name="settings_playback_render_type_cues">Estándar (Cues)</string>
<string name="settings_playback_render_type_cues">Estándar (marcas)</string>
<string name="settings_playback_render_type_effects_canvas">Canvas con efectos</string>
<string name="settings_playback_render_type_effects_opengl">OpenGL con efectos</string>
<string name="settings_playback_render_type_overlay_canvas">Canvas superpuesto</string>
@ -687,7 +687,7 @@
<string name="settings_playback_threshold_mode_percentage">Porcentaje</string>
<string name="settings_playback_threshold_percentage">Porcentaje de umbral</string>
<string name="settings_playback_threshold_percentage_description">Mostrar la tarjeta del siguiente episodio cuando la reproducción alcance este porcentaje.</string>
<string name="settings_playback_threshold_percentage_value">%1$d%%</string>
<string name="settings_playback_threshold_percentage_value">%1$d%</string>
<string name="settings_playback_timeout_instant">Instantáneo</string>
<string name="settings_playback_timeout_seconds">%1$ds</string>
<string name="settings_playback_timeout_unlimited">Ilimitado</string>
@ -877,7 +877,7 @@
<string name="discover_empty_load_failed_message">El catálogo seleccionado no devolvió elementos de descubrimiento.</string>
<string name="discover_empty_load_failed_title">No se pudo cargar Descubrir</string>
<string name="discover_empty_no_catalogs_message">Los addons instalados no exponen catálogos compatibles con el tablero para Descubrir.</string>
<string name="discover_empty_no_catalogs_title">No hay catálogos de descubrir</string>
<string name="discover_empty_no_catalogs_title">No hay catálogos de Descubrir</string>
<string name="discover_empty_no_results_message">El catálogo y los filtros seleccionados no devolvieron ningún elemento.</string>
<string name="discover_empty_no_results_title">No se encontraron títulos</string>
<string name="discover_empty_no_active_addons_message">Instala y valida al menos un addon antes de explorar catálogos en Descubrir.</string>
@ -892,7 +892,7 @@
<string name="episode_mark_unwatched">Marcar como no visto</string>
<string name="episode_mark_watched">Marcar como visto</string>
<string name="home_continue_watching_up_next">Siguiente</string>
<string name="home_continue_watching_watched">%1$d%% visto</string>
<string name="home_continue_watching_watched">%1$s visto</string>
<string name="home_empty_no_active_addons_message">Instala y valida al menos un addon antes de cargar filas de catálogo en Inicio.</string>
<string name="home_empty_no_rows_message">Los addons instalados no exponen actualmente catálogos compatibles con el tablero sin extras requeridos.</string>
<string name="home_empty_no_rows_title">No hay filas de inicio disponibles</string>
@ -948,8 +948,8 @@
<string name="profile_manage_profiles">Gestionar perfiles</string>
<string name="profile_name_placeholder">Nombre del perfil</string>
<string name="profile_new">Perfil nuevo</string>
<string name="profile_primary_addons_off">Addons principales desactivados</string>
<string name="profile_primary_addons_on">Addons principales activados</string>
<string name="profile_primary_addons_off">Complementos principales desactivados</string>
<string name="profile_primary_addons_on">Complementos principales activados</string>
<string name="profile_remove_pin_for">Quitar PIN para %1$s</string>
<string name="profile_remove_pin_lock">Quitar bloqueo PIN</string>
<string name="profile_saving">Guardando...</string>
@ -984,7 +984,7 @@
<string name="streams_no_direct_link">No hay enlace directo del stream disponible</string>
<string name="streams_no_metadata">No hay metadatos disponibles</string>
<string name="streams_refresh">Actualizar streams</string>
<string name="streams_resume_from_percent">Reanudar desde %1$d%%</string>
<string name="streams_resume_from_percent">Reanudar desde %1$d%</string>
<string name="streams_resume_from_time">Reanudar desde %1$s</string>
<string name="streams_size">TAMAÑO %1$s</string>
<string name="trailer_close">Cerrar tráiler</string>
@ -994,7 +994,7 @@
<string name="updates_asset_line">%1$s • %2$s</string>
<string name="updates_check_failed">Falló la comprobación de actualizaciones</string>
<string name="updates_download_failed">La descarga falló</string>
<string name="updates_downloading_progress">Descargando %1$d%%</string>
<string name="updates_downloading_progress">Descargando %1$d%</string>
<string name="updates_install_failed">No se pudo iniciar la instalación</string>
<string name="updates_latest_version">Estás usando la versión más reciente.</string>
<string name="updates_message_allow_installs">Activa la instalación de apps para Nuvio y luego vuelve para continuar.</string>
@ -1021,7 +1021,7 @@
<string name="detail_logo_content_description">logotipo de %1$s</string>
<string name="details_comments_load_failed">No se pudieron cargar los comentarios</string>
<string name="details_load_failed_all_addons">No se pudieron cargar los detalles desde ningún complemento.</string>
<string name="details_networks">Redes</string>
<string name="details_networks">Cadenas</string>
<string name="details_no_addon_meta">Ningún complemento proporciona metadatos para este contenido.</string>
<string name="download_failed">Descarga fallida</string>
<string name="downloads_channel_description">Muestra el progreso en vivo y los controles de descarga.</string>
@ -1039,7 +1039,7 @@
<string name="notifications_test_preview_body">Vista previa de la alerta de estreno de episodio.</string>
<string name="notifications_test_send_failed">No se pudo enviar una notificación de prueba.</string>
<string name="notifications_test_sent_for">Notificación de prueba enviada para %1$s.</string>
<string name="player_unable_to_play_stream">No se puede reproducir esta transmisión.</string>
<string name="player_unable_to_play_stream">No se puede reproducir este stream.</string>
<string name="profile_pin_changed_requires_refresh">El PIN de este perfil cambió. Conéctate una vez para actualizar el bloqueo en este dispositivo.</string>
<string name="profile_pin_clear_failed">No se pudo quitar el bloqueo por PIN. Inténtalo de nuevo.</string>
<string name="profile_pin_clear_requires_internet">Conéctate a internet para quitar el bloqueo por PIN.</string>
@ -1107,7 +1107,7 @@
<string name="details_browse_rail_popular">Popular</string>
<string name="details_browse_rail_recent">Reciente</string>
<string name="details_browse_rail_title">%1$s • %2$s</string>
<string name="details_browse_rail_top_rated">Mejor valorado</string>
<string name="details_browse_rail_top_rated">Mejor valorados</string>
<string name="details_certification">Clasificación</string>
<string name="details_movie_details">Detalles de la película</string>
<string name="details_original_language">Idioma original</string>

File diff suppressed because it is too large Load diff

View file

@ -29,11 +29,11 @@
<string name="addons_delete">Cancella addon</string>
<string name="addons_empty_subtitle">Aggiungi un manifest URL per iniziare a caricare cataloghi , metadata, flussi o sottotitoli dentro Nuvio.</string>
<string name="addons_empty_title">Nessun addon installato ancora.</string>
<string name="addons_error_enter_url">Inserisci l\'URL dell\'addon.</string>
<string name="addons_error_enter_url">Inserisci l'URL dell'addon.</string>
<string name="addons_input_placeholder">URL Addon</string>
<string name="addons_install_button">Installa Addon</string>
<string name="addons_loading_manifest_details">Caricamento dettagli manifest...</string>
<string name="addons_modal_checking_message">Validazione dell\'URL del manifest e caricamento dei dettagli dell\'addon prima dell\'installazione.</string>
<string name="addons_modal_checking_message">Validazione dell'URL del manifest e caricamento dei dettagli dell'addon prima dell'installazione.</string>
<string name="addons_modal_checking_title">Verifica Addon</string>
<string name="addons_modal_failure_title">Installazione Fallita</string>
<string name="addons_modal_success_message">%1$s è stato validato e aggiunto con successo.</string>
@ -52,7 +52,7 @@
<string name="cd_selected">Selezionato</string>
<string name="collections_copy_json">Copia JSON</string>
<string name="collections_count_summary">%1$d collezioni, %2$d cartelle</string>
<string name="collections_delete_message">Eliminare "%1$s"? L\'azione è irreversibile.</string>
<string name="collections_delete_message">Eliminare "%1$s"? L'azione è irreversibile.</string>
<string name="collections_delete_title">Elimina Collezione</string>
<string name="collections_editor_add_catalog">Aggiungi Catalogo</string>
<string name="collections_editor_add_folder">Aggiungi Cartella</string>
@ -68,15 +68,15 @@
<string name="collections_editor_done">Fatto</string>
<string name="collections_editor_edit_collection">Modifica Collezione</string>
<string name="collections_editor_edit_folder">Modifica Cartella</string>
<string name="collections_editor_folder_editor_help">Imposta l\'identità della cartella, la presentazione e le sorgenti del catalogo con la stessa struttura dell\'editor principale delle collezioni.</string>
<string name="collections_editor_folder_editor_help">Imposta l'identità della cartella, la presentazione e le sorgenti del catalogo con la stessa struttura dell'editor principale delle collezioni.</string>
<string name="collections_editor_folder_empty_subtitle">Aggiungine una per iniziare.</string>
<string name="collections_editor_folder_empty_title">Ancora nessuna cartella</string>
<string name="collections_editor_folders">Cartelle</string>
<string name="collections_editor_genre_filter">Filtro Genere</string>
<string name="collections_editor_hide_title_desc">Mostra solo l\'immagine di copertina</string>
<string name="collections_editor_hide_title_desc">Mostra solo l'immagine di copertina</string>
<string name="collections_editor_hide_title">Nascondi Titolo</string>
<string name="collections_editor_new_folder">Nuova Cartella</string>
<string name="collections_editor_pin_above_desc">Mostra questa collezione sopra tutti i normali cataloghi della home. In presenza di multiple collezioni fissate si seguirà l\'ordine di creazione.</string>
<string name="collections_editor_pin_above_desc">Mostra questa collezione sopra tutti i normali cataloghi della home. In presenza di multiple collezioni fissate si seguirà l'ordine di creazione.</string>
<string name="collections_editor_pin_above">Fissa sopra i cataloghi</string>
<string name="collections_editor_placeholder_backdrop">URL backdrop (opzionale)</string>
<string name="collections_editor_placeholder_folder">Nome cartella</string>
@ -229,14 +229,14 @@
<string name="compose_settings_root_account_description">Gestisci il tuo account, disconnettiti o eliminalo.</string>
<string name="compose_settings_root_account_section">ACCOUNT</string>
<string name="compose_settings_root_appearance_description">Regola la presentazione della home e le preferenze visive.</string>
<string name="compose_settings_root_check_updates_description">Controlla se ci sono nuove versioni dell\'app.</string>
<string name="compose_settings_root_check_updates_description">Controlla se ci sono nuove versioni dell'app.</string>
<string name="compose_settings_root_check_updates_title">Verifica aggiornamenti</string>
<string name="compose_settings_root_content_discovery_description">Gestisci gli addon e le sorgenti di scoperta.</string>
<string name="compose_settings_root_downloads_description">Gestisci i film e gli episodi scaricati.</string>
<string name="compose_settings_root_downloads_title">Download</string>
<string name="compose_settings_root_general_section">GENERALI</string>
<string name="compose_settings_root_integrations_description">Collega i servizi TMDB e MDBList.</string>
<string name="compose_settings_root_notifications_description">Gestisci gli avvisi per l\'uscita di nuovi episodi e invia una notifica di test.</string>
<string name="compose_settings_root_notifications_description">Gestisci gli avvisi per l'uscita di nuovi episodi e invia una notifica di test.</string>
<string name="compose_settings_root_switch_profile_description">Passa a un profilo diverso.</string>
<string name="compose_settings_root_switch_profile_title">Cambia profilo</string>
<string name="compose_settings_root_trakt_description">Collega Trakt, sincronizza la lista dei desideri e salva i titoli direttamente su Trakt.</string>
@ -245,7 +245,7 @@
<string name="action_donate">Dona</string>
<string name="cw_action_go_to_details">Vai ai dettagli</string>
<string name="cw_action_remove">Rimuovi</string>
<string name="cw_action_start_from_beginning">Riproduci dall\'inizio</string>
<string name="cw_action_start_from_beginning">Riproduci dall'inizio</string>
<string name="detail_btn_play">Riproduci</string>
<string name="detail_comments_badge_rating">%1$d/10</string>
<string name="detail_comments_badge_review">Recensione</string>
@ -284,7 +284,7 @@
<string name="settings_account_delete_account">Elimina account</string>
<string name="settings_account_delete_account_description">Questo eliminerà permanentemente il tuo account e tutti i dati associati.</string>
<string name="settings_account_delete_confirm_message">Questa azione non può essere annullata. Tutti i tuoi dati, profili e la cronologia di sincronizzazione saranno rimossi per sempre.</string>
<string name="settings_account_delete_confirm_title">Eliminare l\'account?</string>
<string name="settings_account_delete_confirm_title">Eliminare l'account?</string>
<string name="settings_account_email">Email</string>
<string name="settings_account_not_signed_in">Accesso non effettuato</string>
<string name="settings_account_sign_out">Disconnetti</string>
@ -332,7 +332,7 @@
<string name="settings_poster_card_style">STILE LOCANDINA</string>
<string name="settings_poster_card_width">Larghezza locandina</string>
<string name="settings_poster_custom">Personalizzato</string>
<string name="settings_poster_description">Personalizza la larghezza e il raggio degli angoli delle locandine in tutta l\'app.</string>
<string name="settings_poster_description">Personalizza la larghezza e il raggio degli angoli delle locandine in tutta l'app.</string>
<string name="settings_poster_hide_labels">Nascondi etichette</string>
<string name="settings_poster_landscape_mode">Modalità orizzontale per le locandine della riga</string>
<string name="settings_poster_live_preview">Anteprima in tempo reale</string>
@ -351,10 +351,10 @@
<string name="settings_poster_width_dense">Denso</string>
<string name="settings_poster_width_large">Grande</string>
<string name="settings_poster_width_standard">Standard</string>
<string name="settings_continue_watching_resume_prompt_description">Mostra un popup per riprendere la visione all\'apertura dell\'app se eri uscito dal player.</string>
<string name="settings_continue_watching_resume_prompt_title">Richiesta ripresa all\'avvio</string>
<string name="settings_continue_watching_resume_prompt_description">Mostra un popup per riprendere la visione all'apertura dell'app se eri uscito dal player.</string>
<string name="settings_continue_watching_resume_prompt_title">Richiesta ripresa all'avvio</string>
<string name="settings_continue_watching_section_card_style">STILE SCHEDA</string>
<string name="settings_continue_watching_section_on_launch">ALL\'AVVIO</string>
<string name="settings_continue_watching_section_on_launch">ALL'AVVIO</string>
<string name="settings_continue_watching_section_up_next_behavior">COMPORTAMENTO \"PROSSIMO EPISODIO\"</string>
<string name="settings_continue_watching_section_visibility">VISIBILITÀ</string>
<string name="settings_continue_watching_show_description">Mostra la riga \"Continua a guardare\" nella schermata Home.</string>
@ -363,14 +363,14 @@
<string name="settings_continue_watching_style_poster_description">Scheda focalizzata sulla locandina</string>
<string name="settings_continue_watching_style_wide">Orizzontale</string>
<string name="settings_continue_watching_style_wide_description">Scheda orizzontale ricca di informazioni</string>
<string name="settings_continue_watching_up_next_description">Se abilitato, \"Prossimo episodio\" continua sempre dall\'ultimo episodio visto. Se disabilitato, segue l\'episodio visto più di recente. Utile se riguardi spesso episodi precedenti.</string>
<string name="settings_continue_watching_up_next_title">Prossimo episodio dall\'ultimo visto</string>
<string name="settings_continue_watching_up_next_description">Se abilitato, \"Prossimo episodio\" continua sempre dall'ultimo episodio visto. Se disabilitato, segue l'episodio visto più di recente. Utile se riguardi spesso episodi precedenti.</string>
<string name="settings_continue_watching_up_next_title">Prossimo episodio dall'ultimo visto</string>
<string name="settings_content_discovery_section_home">HOME</string>
<string name="settings_content_discovery_section_sources">SORGENTI</string>
<string name="settings_content_discovery_addons_description">Installa, rimuovi, aggiorna e ordina le tue sorgenti di contenuto.</string>
<string name="settings_content_discovery_plugins_description">Installa repository di scraper JavaScript e testa i provider internamente.</string>
<string name="settings_content_discovery_homescreen_description">Controlla quali cataloghi appaiono in Home e in quale ordine.</string>
<string name="settings_content_discovery_meta_screen_description">Disabilita le sezioni dei dettagli e riordina tutto ciò che sta sotto l\'elemento Hero.</string>
<string name="settings_content_discovery_meta_screen_description">Disabilita le sezioni dei dettagli e riordina tutto ciò che sta sotto l'elemento Hero.</string>
<string name="settings_content_discovery_collections_description">Crea raggruppamenti di cataloghi personalizzati con cartelle mostrate in Home.</string>
<string name="settings_integrations_section_title">INTEGRAZIONI</string>
<string name="settings_integrations_tmdb_description">Migliora le pagine dei dettagli con immagini, crediti, metadati degli episodi di TMDB e altro ancora.</string>
@ -416,7 +416,7 @@
<string name="settings_meta_section_sections">SEZIONI</string>
<string name="settings_meta_tab_group_format">Gruppo schede %1$d</string>
<string name="settings_meta_tab_layout">Layout a schede</string>
<string name="settings_meta_tab_layout_description">Raggruppa le sezioni in schede (tab) come nell\'app TV. Assegna fino a 3 sezioni per ogni gruppo.</string>
<string name="settings_meta_tab_layout_description">Raggruppa le sezioni in schede (tab) come nell'app TV. Assegna fino a 3 sezioni per ogni gruppo.</string>
<string name="settings_meta_trailers">Trailer</string>
<string name="settings_meta_trailers_description">Riga dei trailer e scorciatoie di riproduzione.</string>
<string name="settings_notifications_disabled_in_app">Le notifiche sono attualmente disabilitate in Nuvio.</string>
@ -433,7 +433,7 @@
<string name="settings_notifications_test_title">Notifica di test</string>
<string name="community_section_title">Community</string>
<string name="community_section_description">Scopri le persone che sviluppano e supportano Nuvio su Mobile, TV e Web.</string>
<string name="community_supporters_not_configured">L\'API dei sostenitori non è configurata. Aggiungi DONATIONS_BASE_URL a local.properties.</string>
<string name="community_supporters_not_configured">L'API dei sostenitori non è configurata. Aggiungi DONATIONS_BASE_URL a local.properties.</string>
<string name="community_tab_contributors">Collaboratori</string>
<string name="community_tab_supporters">Sostenitori</string>
<string name="community_open_github">Apri GitHub</string>
@ -472,7 +472,7 @@
<string name="settings_playback_anime_skip_client_id_description">Inserisci il tuo ID client API AnimeSkip. Ottienine uno su anime-skip.com.</string>
<string name="settings_playback_anime_skip_description">Cerca anche su AnimeSkip i timestamp per saltare le sigle (richiede ID client).</string>
<string name="settings_playback_auto_play_next_episode">Riproduzione automatica prossimo episodio</string>
<string name="settings_playback_auto_play_next_episode_description">Trova e riproduce automaticamente l\'episodio successivo al raggiungimento della soglia.</string>
<string name="settings_playback_auto_play_next_episode_description">Trova e riproduce automaticamente l'episodio successivo al raggiungimento della soglia.</string>
<string name="settings_playback_decoder_device_only">Solo dispositivo</string>
<string name="settings_playback_decoder_prefer_app">Preferisci App (FFmpeg)</string>
<string name="settings_playback_decoder_prefer_device">Preferisci dispositivo</string>
@ -493,7 +493,7 @@
<string name="settings_playback_map_dv7_to_hevc">Mappa DV7 su HEVC</string>
<string name="settings_playback_map_dv7_to_hevc_description">Fallback da Dolby Vision Profile 7 a HEVC per i dispositivi non supportati.</string>
<string name="settings_playback_minutes_before_end">Minuti prima della fine</string>
<string name="settings_playback_minutes_before_end_description">Mostra la scheda dell\'episodio successivo questo numero di minuti prima della fine.</string>
<string name="settings_playback_minutes_before_end_description">Mostra la scheda dell'episodio successivo questo numero di minuti prima della fine.</string>
<string name="settings_playback_minutes_value">%1$d min</string>
<string name="settings_playback_no_items_available">Nessun elemento disponibile</string>
<string name="settings_playback_not_set">Non impostato</string>
@ -528,8 +528,8 @@
<string name="settings_playback_render_type_effects_opengl">Effetti OpenGL</string>
<string name="settings_playback_render_type_overlay_canvas">Overlay Canvas</string>
<string name="settings_playback_render_type_overlay_opengl">Overlay OpenGL</string>
<string name="settings_playback_reuse_last_link">Riusa l\'ultimo link</string>
<string name="settings_playback_reuse_last_link_description">Riproduci automaticamente l\'ultimo flusso funzionante per lo stesso film/episodio se la cache è ancora valida.</string>
<string name="settings_playback_reuse_last_link">Riusa l'ultimo link</string>
<string name="settings_playback_reuse_last_link_description">Riproduci automaticamente l'ultimo flusso funzionante per lo stesso film/episodio se la cache è ancora valida.</string>
<string name="settings_playback_secondary_audio_language">Lingua audio secondaria</string>
<string name="settings_playback_secondary_subtitle_language">Lingua sottotitoli secondaria</string>
<string name="settings_playback_section_decoder">DECODER</string>
@ -542,7 +542,7 @@
<string name="settings_playback_section_subtitle_rendering">RENDERING SOTTOTITOLI</string>
<string name="settings_playback_selected_count">%1$d selezionati</string>
<string name="settings_playback_show_loading_overlay">Mostra overlay di caricamento</string>
<string name="settings_playback_show_loading_overlay_description">Mostra una schermata di caricamento all\'avvio della riproduzione di un flusso.</string>
<string name="settings_playback_show_loading_overlay_description">Mostra una schermata di caricamento all'avvio della riproduzione di un flusso.</string>
<string name="settings_playback_skip_intro_outro_recap">Salta Intro/Outro/Recap</string>
<string name="settings_playback_skip_intro_outro_recap_description">Mostra il pulsante \"salta\" durante i segmenti rilevati di introduzione, chiusura e riassunto.</string>
<string name="settings_playback_source_scope">Ambito sorgente</string>
@ -568,14 +568,14 @@
<string name="settings_playback_threshold_mode_minutes_before_end">Minuti prima della fine</string>
<string name="settings_playback_threshold_mode_percentage">Percentuale</string>
<string name="settings_playback_threshold_percentage">Percentuale di soglia</string>
<string name="settings_playback_threshold_percentage_description">Mostra la scheda dell\'episodio successivo quando la riproduzione raggiunge questa percentuale.</string>
<string name="settings_playback_threshold_percentage_value">%1$d%%</string>
<string name="settings_playback_threshold_percentage_description">Mostra la scheda dell'episodio successivo quando la riproduzione raggiunge questa percentuale.</string>
<string name="settings_playback_threshold_percentage_value">%1$d%</string>
<string name="settings_playback_timeout_instant">Istantaneo</string>
<string name="settings_playback_timeout_seconds">%1$ds</string>
<string name="settings_playback_timeout_unlimited">Illimitato</string>
<string name="settings_playback_tunneled_playback">Riproduzione Tunneled</string>
<string name="settings_playback_tunneled_playback_description">Abilita la riproduzione tunneled per una minore latenza nella sincronizzazione audio/video.</string>
<string name="settings_tmdb_add_api_key_first">Aggiungi la tua chiave API TMDB qui sotto prima di attivare l\'arricchimento.</string>
<string name="settings_tmdb_add_api_key_first">Aggiungi la tua chiave API TMDB qui sotto prima di attivare l'arricchimento.</string>
<string name="settings_tmdb_api_key_label">Chiave API TMDB</string>
<string name="settings_tmdb_enable_enrichment">Abilita arricchimento TMDB</string>
<string name="settings_tmdb_enable_enrichment_description">Usa la tua chiave API TMDB per arricchire i metadati degli addon nella schermata dei dettagli quando è disponibile un ID TMDB o IMDb.</string>
@ -590,7 +590,7 @@
<string name="settings_tmdb_module_credits">Crediti</string>
<string name="settings_tmdb_module_credits_description">Usa creatori, registi, sceneggiatori e foto del cast di TMDB.</string>
<string name="settings_tmdb_module_details">Dettagli</string>
<string name="settings_tmdb_module_details_description">Usa info su rilascio, durata, classificazione d\'età, stato, paese e lingua di TMDB.</string>
<string name="settings_tmdb_module_details_description">Usa info su rilascio, durata, classificazione d'età, stato, paese e lingua di TMDB.</string>
<string name="settings_tmdb_module_episodes">Episodi</string>
<string name="settings_tmdb_module_episodes_description">Usa titoli, miniature, descrizioni e durate degli episodi di TMDB per le serie.</string>
<string name="settings_tmdb_module_more_like_this">Altri titoli simili</string>
@ -610,7 +610,7 @@
<string name="settings_tmdb_section_localization">LOCALIZZAZIONE</string>
<string name="settings_tmdb_section_modules">MODULI</string>
<string name="settings_tmdb_section_title">TMDB</string>
<string name="settings_trakt_approval_redirect">Dopo l\'approvazione, verrai reindirizzato automaticamente.</string>
<string name="settings_trakt_approval_redirect">Dopo l'approvazione, verrai reindirizzato automaticamente.</string>
<string name="settings_trakt_authentication">AUTENTICAZIONE</string>
<string name="settings_trakt_comments">Commenti</string>
<string name="settings_trakt_comments_description">Mostra i commenti di Trakt nei dettagli di film e serie TV.</string>
@ -620,7 +620,7 @@
<string name="settings_trakt_disconnect">Disconnetti</string>
<string name="settings_trakt_failed_open_browser">Impossibile aprire il browser</string>
<string name="settings_trakt_features">FUNZIONALITÀ</string>
<string name="settings_trakt_finish_sign_in">Completa l\'accesso a Trakt nel tuo browser</string>
<string name="settings_trakt_finish_sign_in">Completa l'accesso a Trakt nel tuo browser</string>
<string name="settings_trakt_intro_description">Tieni traccia di ciò che guardi, salva contenuti nella watchlist o in liste personalizzate e mantieni la tua libreria sincronizzata con Trakt.</string>
<string name="settings_trakt_missing_credentials">Credenziali Trakt mancanti in local.properties (TRAKT_CLIENT_ID / TRAKT_CLIENT_SECRET).</string>
<string name="settings_trakt_open_login">Apri Login Trakt</string>
@ -737,8 +737,8 @@
<string name="action_no">No</string>
<string name="action_update">Aggiorna</string>
<string name="action_yes"></string>
<string name="app_exit_message">Vuoi uscire dall\'app?</string>
<string name="app_exit_title">Esci dall\'app</string>
<string name="app_exit_message">Vuoi uscire dall'app?</string>
<string name="app_exit_title">Esci dall'app</string>
<string name="catalog_empty_message">Questo catalogo non ha restituito alcun elemento.</string>
<string name="catalog_empty_title">Nessun titolo trovato</string>
<string name="details_check_connection">Controlla la tua connessione Wi-Fi o dati mobili e riprova.</string>
@ -746,8 +746,8 @@
<string name="details_failed_to_load">Caricamento fallito</string>
<string name="details_more_like_this">Altri titoli simili</string>
<string name="details_seasons">Stagioni</string>
<string name="details_series_missing_numbers">L\'addon ha restituito i video per questa serie, ma nessuno include numeri di stagione o episodio.</string>
<string name="details_series_no_metadata">L\'addon non ha fornito metadati sugli episodi per questa serie.</string>
<string name="details_series_missing_numbers">L'addon ha restituito i video per questa serie, ma nessuno include numeri di stagione o episodio.</string>
<string name="details_series_no_metadata">L'addon non ha fornito metadati sugli episodi per questa serie.</string>
<string name="details_series_unpublished">Gli episodi non sono ancora stati pubblicati da questo addon.</string>
<string name="details_servers_unreachable">Il dispositivo è online, ma Nuvio non è riuscito a raggiungere i server richiesti.</string>
<string name="details_show_less">Mostra meno</string>
@ -774,7 +774,7 @@
<string name="episode_mark_unwatched">Segna come non visto</string>
<string name="episode_mark_watched">Segna come visto</string>
<string name="home_continue_watching_up_next">Prossimo episodio</string>
<string name="home_continue_watching_watched">%1$d%% visto</string>
<string name="home_continue_watching_watched">%1$s visto</string>
<string name="home_empty_no_active_addons_message">Installa e convalida almeno un addon prima di caricare le righe del catalogo in Home.</string>
<string name="home_empty_no_rows_message">Gli addon installati non espongono attualmente cataloghi compatibili con la bacheca senza gli extra richiesti.</string>
<string name="home_empty_no_rows_title">Nessuna riga disponibile in Home</string>
@ -866,7 +866,7 @@
<string name="streams_no_direct_link">Nessun link diretto disponibile</string>
<string name="streams_no_metadata">Nessun metadato disponibile</string>
<string name="streams_refresh">Aggiorna flussi</string>
<string name="streams_resume_from_percent">Riprendi dal %1$d%%</string>
<string name="streams_resume_from_percent">Riprendi dal %1$d%</string>
<string name="streams_resume_from_time">Riprendi da %1$s</string>
<string name="streams_size">DIMENSIONE %1$s</string>
<string name="trailer_close">Chiudi trailer</string>
@ -876,13 +876,13 @@
<string name="updates_asset_line">%1$s • %2$s</string>
<string name="updates_check_failed">Controllo aggiornamenti fallito</string>
<string name="updates_download_failed">Download fallito</string>
<string name="updates_downloading_progress">Download in corso: %1$d%%</string>
<string name="updates_install_failed">Impossibile avviare l\'installazione</string>
<string name="updates_latest_version">Stai utilizzando l\'ultima versione.</string>
<string name="updates_message_allow_installs">Abilita l\'installazione di app per Nuvio, quindi torna qui e continua.</string>
<string name="updates_downloading_progress">Download in corso: %1$d%</string>
<string name="updates_install_failed">Impossibile avviare l'installazione</string>
<string name="updates_latest_version">Stai utilizzando l'ultima versione.</string>
<string name="updates_message_allow_installs">Abilita l'installazione di app per Nuvio, quindi torna qui e continua.</string>
<string name="updates_message_downloading">Download aggiornamento in corso...</string>
<string name="updates_message_no_updates">Nessun aggiornamento trovato.</string>
<string name="updates_message_ready">Una nuova versione è pronta per l\'installazione.</string>
<string name="updates_message_ready">Una nuova versione è pronta per l'installazione.</string>
<string name="updates_not_available">Gli aggiornamenti in-app non sono disponibili in questa versione.</string>
<string name="updates_preparing_download">Preparazione del download</string>
<string name="updates_release_notes">Note di rilascio</string>
@ -918,7 +918,7 @@
<string name="library_remove_title">Rimuovere dalla libreria?</string>
<string name="media_movie">Film</string>
<string name="notifications_channel_episode_releases_description">Avvisi quando viene rilasciato un nuovo episodio di una serie salvata.</string>
<string name="notifications_test_preview_body">Anteprima dell\'avviso di uscita episodio.</string>
<string name="notifications_test_preview_body">Anteprima dell'avviso di uscita episodio.</string>
<string name="notifications_test_send_failed">Impossibile inviare la notifica di test.</string>
<string name="notifications_test_sent_for">Notifica di test inviata per %1$s.</string>
<string name="player_unable_to_play_stream">Impossibile riprodurre questo flusso.</string>
@ -933,7 +933,7 @@
<string name="stream_default_name">Flusso</string>
<string name="source_embedded">Incorporato (Embedded)</string>
<string name="trakt_authorization_denied">Autorizzazione negata</string>
<string name="trakt_complete_sign_in_browser">Completa l\'accesso a Trakt nel tuo browser</string>
<string name="trakt_complete_sign_in_browser">Completa l'accesso a Trakt nel tuo browser</string>
<string name="trakt_invalid_callback">Callback Trakt non valido</string>
<string name="trakt_invalid_callback_state">Stato callback Trakt non valido</string>
<string name="trakt_invalid_token_response">Risposta token Trakt non valida</string>
@ -942,7 +942,7 @@
<string name="trakt_missing_auth_code">Trakt non ha restituito un codice di autorizzazione</string>
<string name="trakt_missing_credentials">Credenziali Trakt mancanti</string>
<string name="trakt_progress_load_failed">Impossibile caricare i progressi di Trakt</string>
<string name="trakt_sign_in_complete_failed">Impossibile completare l\'accesso a Trakt</string>
<string name="trakt_sign_in_complete_failed">Impossibile completare l'accesso a Trakt</string>
<string name="trakt_user_fallback">Utente Trakt</string>
<string name="trakt_watchlist">Watchlist</string>
<string name="generic_trailer">Trailer</string>
@ -953,10 +953,10 @@
<string name="action_resume_episode">Riprendi %1$s</string>
<string name="collections_import_error_empty_json">Il file JSON è vuoto.</string>
<string name="collections_import_error_collection_blank_id">La collezione %1$d ha un ID vuoto.</string>
<string name="collections_import_error_collection_blank_title">La collezione \'%1$s\' ha un titolo vuoto.</string>
<string name="collections_import_error_folder_blank_id">La cartella %1$d in \'%2$s\' ha un ID vuoto.</string>
<string name="collections_import_error_folder_blank_title">La cartella \'%1$s\' in \'%2$s\' ha un titolo vuoto.</string>
<string name="collections_import_error_source_blank_fields">La sorgente %1$d nella cartella \'%2$s\' presenta campi vuoti.</string>
<string name="collections_import_error_collection_blank_title">La collezione '%1$s' ha un titolo vuoto.</string>
<string name="collections_import_error_folder_blank_id">La cartella %1$d in '%2$s' ha un ID vuoto.</string>
<string name="collections_import_error_folder_blank_title">La cartella '%1$s' in '%2$s' ha un titolo vuoto.</string>
<string name="collections_import_error_source_blank_fields">La sorgente %1$d nella cartella '%2$s' presenta campi vuoti.</string>
<string name="collections_import_error_invalid_json">JSON non valido: %1$s</string>
<string name="collections_folder_addon_not_found">Addon non trovato: %1$s</string>
<string name="date_month_january">Gennaio</string>
@ -993,7 +993,7 @@
<string name="details_certification">Certificazione</string>
<string name="details_movie_details">Dettagli film</string>
<string name="details_original_language">Lingua originale</string>
<string name="details_origin_country">Paese d\'origine</string>
<string name="details_origin_country">Paese d'origine</string>
<string name="details_release_info">Info rilascio</string>
<string name="details_runtime">Durata</string>
<string name="details_season_view_posters">Locandine</string>

View file

@ -687,7 +687,7 @@
<string name="settings_playback_threshold_mode_percentage">Procent</string>
<string name="settings_playback_threshold_percentage">Próg procentowy</string>
<string name="settings_playback_threshold_percentage_description">Pokaż kartę następnego odcinka, gdy odtwarzanie osiągnie ten procent.</string>
<string name="settings_playback_threshold_percentage_value">%1$d%%</string>
<string name="settings_playback_threshold_percentage_value">%1$d%</string>
<string name="settings_playback_timeout_instant">Natychmiast</string>
<string name="settings_playback_timeout_seconds">%1$ds</string>
<string name="settings_playback_timeout_unlimited">Bez limitu</string>
@ -892,7 +892,7 @@
<string name="episode_mark_unwatched">Oznacz jako nieobejrzane</string>
<string name="episode_mark_watched">Oznacz jako obejrzane</string>
<string name="home_continue_watching_up_next">Następny</string>
<string name="home_continue_watching_watched">%1$d%% obejrzane</string>
<string name="home_continue_watching_watched">%1$s obejrzane</string>
<string name="home_empty_no_active_addons_message">Zainstaluj i sprawdź co najmniej jeden dodatek przed ładowaniem wierszy katalogów na ekranie głównym.</string>
<string name="home_empty_no_rows_message">Zainstalowane dodatki nie udostępniają obecnie katalogów kompatybilnych z tablicą bez wymaganych dodatków.</string>
<string name="home_empty_no_rows_title">Brak dostępnych wierszy ekranu głównego</string>
@ -984,7 +984,7 @@
<string name="streams_no_direct_link">Brak bezpośredniego linku strumienia</string>
<string name="streams_no_metadata">Brak dostępnych metadanych</string>
<string name="streams_refresh">Odśwież strumienie</string>
<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_size">ROZMIAR %1$s</string>
<string name="trailer_close">Zamknij zwiastun</string>
@ -994,7 +994,7 @@
<string name="updates_asset_line">%1$s • %2$s</string>
<string name="updates_check_failed">Sprawdzanie aktualizacji nie powiodło się</string>
<string name="updates_download_failed">Pobieranie nie powiodło się</string>
<string name="updates_downloading_progress">Pobieranie %1$d%%</string>
<string name="updates_downloading_progress">Pobieranie %1$d%</string>
<string name="updates_install_failed">Nie można rozpocząć instalacji</string>
<string name="updates_latest_version">Używasz najnowszej wersji.</string>
<string name="updates_message_allow_installs">Zezwól na instalację aplikacji dla Nuvio, a następnie wróć i kontynuuj.</string>
@ -1158,4 +1158,4 @@
<string name="unit_bytes_kb">KB</string>
<string name="unit_bytes_mb">MB</string>
<string name="unit_bytes_gb">GB</string>
</resources>
</resources>

File diff suppressed because it is too large Load diff

View file

@ -569,7 +569,7 @@
<string name="settings_playback_threshold_mode_percentage">Yüzde</string>
<string name="settings_playback_threshold_percentage">Eşik yüzdesi</string>
<string name="settings_playback_threshold_percentage_description">Oynatma bu yüzdeye ulaşınca sonraki bölüm kartını göster.</string>
<string name="settings_playback_threshold_percentage_value">%1$d%%</string>
<string name="settings_playback_threshold_percentage_value">%1$d%</string>
<string name="settings_playback_timeout_instant">Hemen</string>
<string name="settings_playback_timeout_seconds">%1$dsn</string>
<string name="settings_playback_timeout_unlimited">Sınırsız</string>
@ -774,7 +774,7 @@
<string name="episode_mark_unwatched">İzlenmedi olarak işaretle</string>
<string name="episode_mark_watched">İzlendi olarak işaretle</string>
<string name="home_continue_watching_up_next">Sıradaki</string>
<string name="home_continue_watching_watched">%%%1$d izlendi</string>
<string name="home_continue_watching_watched">%1$s izlendi</string>
<string name="home_empty_no_active_addons_message">Ana sayfada katalog satırlarını yüklemeden önce en az bir eklenti kurup doğrula.</string>
<string name="home_empty_no_rows_message">Kurulu eklentiler şu anda gerekli ek bilgiler olmadan ana sayfaya uyumlu katalog sunmuyor.</string>
<string name="home_empty_no_rows_title">Ana sayfa satırı yok</string>
@ -866,7 +866,7 @@
<string name="streams_no_direct_link">Doğrudan yayın bağlantısı yok</string>
<string name="streams_no_metadata">Meta veri yok</string>
<string name="streams_refresh">Yayınları yenile</string>
<string name="streams_resume_from_percent">%%%1$d konumundan devam et</string>
<string name="streams_resume_from_percent">%1$d% konumundan devam et</string>
<string name="streams_resume_from_time">%1$s konumundan devam et</string>
<string name="streams_size">BOYUT %1$s</string>
<string name="trailer_close">Fragmanı kapat</string>
@ -876,7 +876,7 @@
<string name="updates_asset_line">%1$s • %2$s</string>
<string name="updates_check_failed">Güncelleme kontrolü olmadı</string>
<string name="updates_download_failed">İndirme olmadı</string>
<string name="updates_downloading_progress">İndiriliyor %%%1$d</string>
<string name="updates_downloading_progress">İndiriliyor %1$d%</string>
<string name="updates_install_failed">Kurulum başlatılamadı</string>
<string name="updates_latest_version">En güncel sürümü kullanıyorsun.</string>
<string name="updates_message_allow_installs">Nuvio için uygulama kurulumlarına izin ver, sonra geri gelip devam et.</string>

View file

@ -110,29 +110,38 @@
<string name="collections_editor_tmdb_production_mode">Production</string>
<string name="collections_editor_tmdb_network_mode">Network</string>
<string name="collections_editor_tmdb_collection_mode">Collection</string>
<string name="collections_editor_tmdb_person_mode">Person</string>
<string name="collections_editor_tmdb_director_mode">Director</string>
<string name="collections_editor_tmdb_custom_mode">Custom</string>
<string name="collections_editor_tmdb_help_presets">Pick a ready-made source. You can edit or remove it after adding.</string>
<string name="collections_editor_tmdb_help_list">Paste a public TMDB list URL or only the number from the URL.</string>
<string name="collections_editor_tmdb_help_production">Search by studio name, or paste a TMDB company ID/URL and add it directly.</string>
<string name="collections_editor_tmdb_help_network">Enter a network ID. Common networks are available in Presets and quick filters.</string>
<string name="collections_editor_tmdb_help_collection">Search a movie collection name or paste the collection ID from TMDB.</string>
<string name="collections_editor_tmdb_help_person">Enter a TMDB person ID or URL to build a row from cast credits.</string>
<string name="collections_editor_tmdb_help_director">Enter a TMDB person ID or URL to build a row from director credits.</string>
<string name="collections_editor_tmdb_help_discover">Build a live TMDB row using optional filters. Leave fields empty when you do not need that filter.</string>
<string name="collections_editor_tmdb_public_list">Public TMDB list</string>
<string name="collections_editor_tmdb_network_id">Network ID</string>
<string name="collections_editor_tmdb_collection_id">Collection ID</string>
<string name="collections_editor_tmdb_person_id">Person ID</string>
<string name="collections_editor_tmdb_company_search">Production company name, ID, or URL</string>
<string name="collections_editor_tmdb_id_or_url">TMDB ID or URL</string>
<string name="collections_editor_tmdb_list_placeholder">https://www.themoviedb.org/list/8504994 or 8504994</string>
<string name="collections_editor_tmdb_network_placeholder">213 for Netflix, 49 for HBO, 2739 for Disney+</string>
<string name="collections_editor_tmdb_collection_placeholder">10 for Star Wars Collection</string>
<string name="collections_editor_tmdb_company_placeholder">Marvel Studios, 420, or company URL</string>
<string name="collections_editor_tmdb_person_placeholder">31 for Tom Hanks, or person URL</string>
<string name="collections_editor_tmdb_search_helper">Examples: Marvel Studios, 420, or https://www.themoviedb.org/company/420.</string>
<string name="collections_editor_tmdb_collection_helper">Example: Star Wars Collection, Harry Potter Collection, or a collection URL.</string>
<string name="collections_editor_tmdb_network_helper">Example IDs: Netflix 213, HBO 49, Disney+ 2739.</string>
<string name="collections_editor_tmdb_list_helper">Example: https://www.themoviedb.org/list/8504994 or 8504994.</string>
<string name="collections_editor_tmdb_person_helper">Example: https://www.themoviedb.org/person/31-tom-hanks or 31.</string>
<string name="collections_editor_tmdb_display_title">Display title</string>
<string name="collections_editor_tmdb_title_helper">Shown as the row/tab name. If blank, Nuvio creates one from the source.</string>
<string name="collections_editor_tmdb_title_placeholder">Marvel Movies, Netflix Originals, Pixar</string>
<string name="collections_editor_tmdb_person_title_placeholder">Tom Hanks Movies, Favorite Actors</string>
<string name="collections_editor_tmdb_director_title_placeholder">Christopher Nolan Movies, Favorite Directors</string>
<string name="collections_editor_tmdb_discover_title_placeholder">Best Action Movies, Korean Dramas, 2024 Animation</string>
<string name="collections_editor_tmdb_search_results">Search Results</string>
<string name="collections_editor_tmdb_collection">TMDB Collection</string>
@ -212,6 +221,7 @@
<string name="collections_editor_tmdb_network_disney_plus">Disney+</string>
<string name="collections_editor_tmdb_network_prime_video">Prime Video</string>
<string name="collections_editor_tmdb_network_hulu">Hulu</string>
<string name="collections_editor_tmdb_sort_original">Original</string>
<string name="collections_editor_tmdb_sort_popular">Popular</string>
<string name="collections_editor_tmdb_sort_top_rated">Top Rated</string>
<string name="collections_editor_tmdb_sort_recent">Recent</string>
@ -219,6 +229,8 @@
<string name="collections_editor_tmdb_subtitle_movie_collection">TMDB Movie Collection</string>
<string name="collections_editor_tmdb_subtitle_production">Production</string>
<string name="collections_editor_tmdb_subtitle_network">Network</string>
<string name="collections_editor_tmdb_subtitle_person">Person</string>
<string name="collections_editor_tmdb_subtitle_director">Director</string>
<string name="collections_editor_tmdb_subtitle_discover">TMDB Discover</string>
<string name="collections_empty_subtitle">Create one to organize your catalogs.</string>
<string name="collections_empty_title">No collections yet</string>
@ -687,7 +699,7 @@
<string name="settings_playback_threshold_mode_percentage">Percentage</string>
<string name="settings_playback_threshold_percentage">Threshold Percentage</string>
<string name="settings_playback_threshold_percentage_description">Show next episode card when playback reaches this percentage.</string>
<string name="settings_playback_threshold_percentage_value">%1$d%%</string>
<string name="settings_playback_threshold_percentage_value">%1$d%</string>
<string name="settings_playback_timeout_instant">Instant</string>
<string name="settings_playback_timeout_seconds">%1$ds</string>
<string name="settings_playback_timeout_unlimited">Unlimited</string>
@ -892,7 +904,7 @@
<string name="episode_mark_unwatched">Mark as unwatched</string>
<string name="episode_mark_watched">Mark as watched</string>
<string name="home_continue_watching_up_next">Up next</string>
<string name="home_continue_watching_watched">%1$d%% watched</string>
<string name="home_continue_watching_watched">%1$s watched</string>
<string name="home_empty_no_active_addons_message">Install and validate at least one addon before loading catalog rows on Home.</string>
<string name="home_empty_no_rows_message">Installed addons do not currently expose board-compatible catalogs without required extras.</string>
<string name="home_empty_no_rows_title">No home rows available</string>
@ -984,7 +996,7 @@
<string name="streams_no_direct_link">No direct stream link available</string>
<string name="streams_no_metadata">No metadata available</string>
<string name="streams_refresh">Refresh streams</string>
<string name="streams_resume_from_percent">Resume from %1$d%%</string>
<string name="streams_resume_from_percent">Resume from %1$d%</string>
<string name="streams_resume_from_time">Resume from %1$s</string>
<string name="streams_size">SIZE %1$s</string>
<string name="trailer_close">Close trailer</string>
@ -994,7 +1006,7 @@
<string name="updates_asset_line">%1$s • %2$s</string>
<string name="updates_check_failed">Update check failed</string>
<string name="updates_download_failed">Download failed</string>
<string name="updates_downloading_progress">Downloading %1$d%%</string>
<string name="updates_downloading_progress">Downloading %1$d%</string>
<string name="updates_install_failed">Unable to start installation</string>
<string name="updates_latest_version">You&apos;re using the latest version.</string>
<string name="updates_message_allow_installs">Enable app installs for Nuvio, then come back and continue.</string>

View file

@ -0,0 +1,43 @@
package com.nuvio.app.features.collection
import com.nuvio.app.features.addons.AddonCatalog
import com.nuvio.app.features.addons.ManagedAddon
internal data class ResolvedCollectionCatalog(
val addon: ManagedAddon,
val catalog: AddonCatalog,
)
internal fun List<ManagedAddon>.findCollectionCatalog(
source: CollectionCatalogSource,
): ResolvedCollectionCatalog? {
val declaredAddon = firstOrNull { it.manifest?.id == source.addonId }
val declaredCatalog = declaredAddon?.manifest?.catalogs?.findSourceCatalog(source)
if (declaredAddon != null && declaredCatalog != null) {
return ResolvedCollectionCatalog(addon = declaredAddon, catalog = declaredCatalog)
}
return firstNotNullOfOrNull { addon ->
val catalog = addon.manifest?.catalogs?.find {
it.id == source.catalogId && it.type == source.type
} ?: return@firstNotNullOfOrNull null
ResolvedCollectionCatalog(addon = addon, catalog = catalog)
}
}
internal fun List<AvailableCatalog>.findAvailableCatalog(
source: CollectionCatalogSource,
): AvailableCatalog? {
val declaredCatalogs = filter { it.addonId == source.addonId }
return declaredCatalogs.findSourceCatalog(source)
?: firstOrNull { it.catalogId == source.catalogId && it.type == source.type }
}
private fun List<AddonCatalog>.findSourceCatalog(source: CollectionCatalogSource): AddonCatalog? =
find { it.id == source.catalogId && it.type == source.type }
?: find { it.id == source.catalogId.substringBefore(",") && it.type == source.type }
private fun List<AvailableCatalog>.findSourceCatalog(source: CollectionCatalogSource): AvailableCatalog? =
find { it.catalogId == source.catalogId && it.type == source.type }
?: find { it.catalogId == source.catalogId.substringBefore(",") && it.type == source.type }

View file

@ -46,6 +46,8 @@ enum class TmdbBuilderMode {
PRODUCTION,
NETWORK,
COLLECTION,
PERSON,
DIRECTOR,
DISCOVER,
}
@ -340,9 +342,15 @@ object CollectionEditorRepository {
} else {
_uiState.value.tmdbMediaType
}
val sortBy = when (mode) {
TmdbBuilderMode.LIST,
TmdbBuilderMode.COLLECTION -> TmdbCollectionSort.ORIGINAL.value
else -> TmdbCollectionSort.POPULAR_DESC.value
}
_uiState.value = _uiState.value.copy(
tmdbBuilderMode = mode,
tmdbMediaType = mediaType,
tmdbSortBy = sortBy,
tmdbMediaBoth = if (
mode == TmdbBuilderMode.NETWORK ||
mode == TmdbBuilderMode.LIST ||
@ -459,6 +467,8 @@ object CollectionEditorRepository {
TmdbBuilderMode.COLLECTION -> TmdbCollectionSourceType.COLLECTION
TmdbBuilderMode.PRODUCTION -> TmdbCollectionSourceType.COMPANY
TmdbBuilderMode.NETWORK -> TmdbCollectionSourceType.NETWORK
TmdbBuilderMode.PERSON -> TmdbCollectionSourceType.PERSON
TmdbBuilderMode.DIRECTOR -> TmdbCollectionSourceType.DIRECTOR
TmdbBuilderMode.DISCOVER -> TmdbCollectionSourceType.DISCOVER
}
val id = TmdbCollectionSourceResolver.parseTmdbId(state.tmdbInput)
@ -473,6 +483,8 @@ object CollectionEditorRepository {
TmdbCollectionSourceType.COLLECTION -> "TMDB Collection ${id ?: ""}".trim()
TmdbCollectionSourceType.COMPANY -> "TMDB Production ${id ?: ""}".trim()
TmdbCollectionSourceType.NETWORK -> "TMDB Network ${id ?: ""}".trim()
TmdbCollectionSourceType.PERSON -> "TMDB Person ${id ?: ""}".trim()
TmdbCollectionSourceType.DIRECTOR -> "TMDB Director ${id ?: ""}".trim()
TmdbCollectionSourceType.DISCOVER -> "TMDB Discover"
}
}
@ -561,6 +573,8 @@ private val coverMetadataSourceTypes = setOf(
TmdbCollectionSourceType.COLLECTION,
TmdbCollectionSourceType.COMPANY,
TmdbCollectionSourceType.NETWORK,
TmdbCollectionSourceType.PERSON,
TmdbCollectionSourceType.DIRECTOR,
)
private fun CollectionCatalogSource.toCollectionSource(): CollectionSource =
@ -591,6 +605,8 @@ private fun selectedMediaTypes(
): List<TmdbCollectionMediaType> =
when (sourceType) {
TmdbCollectionSourceType.COMPANY,
TmdbCollectionSourceType.PERSON,
TmdbCollectionSourceType.DIRECTOR,
TmdbCollectionSourceType.DISCOVER -> if (state.tmdbMediaBoth) {
listOf(TmdbCollectionMediaType.MOVIE, TmdbCollectionMediaType.TV)
} else {

View file

@ -111,9 +111,7 @@ fun CollectionEditorScreen(
val genrePickerSource = genrePickerIndex?.let { editingFolder.resolvedSources.getOrNull(it) }
val genrePickerCatalogSource = genrePickerSource?.addonCatalogSource()
val genrePickerCatalog = genrePickerCatalogSource?.let { source ->
state.availableCatalogs.find {
it.addonId == source.addonId && it.type == source.type && it.catalogId == source.catalogId
}
state.availableCatalogs.findAvailableCatalog(source)
}
FolderEditorPage(
@ -757,11 +755,7 @@ private fun FolderEditorPage(
} else if (addonSource != null) {
FolderCatalogSourceCard(
source = addonSource,
matchingCatalog = state.availableCatalogs.find {
it.addonId == addonSource.addonId &&
it.type == addonSource.type &&
it.catalogId == addonSource.catalogId
},
matchingCatalog = state.availableCatalogs.findAvailableCatalog(addonSource),
onRemove = { CollectionEditorRepository.removeCatalogSource(index) },
onOpenGenrePicker = { CollectionEditorRepository.showGenrePicker(index) },
)
@ -897,13 +891,19 @@ private fun TmdbSourcePickerScreen(
TmdbBuilderMode.COLLECTION -> TmdbCollectionSourceType.COLLECTION
TmdbBuilderMode.PRODUCTION -> TmdbCollectionSourceType.COMPANY
TmdbBuilderMode.NETWORK -> TmdbCollectionSourceType.NETWORK
TmdbBuilderMode.PERSON -> TmdbCollectionSourceType.PERSON
TmdbBuilderMode.DIRECTOR -> TmdbCollectionSourceType.DIRECTOR
TmdbBuilderMode.DISCOVER -> TmdbCollectionSourceType.DISCOVER
}
val requiresId = sourceType != TmdbCollectionSourceType.DISCOVER
val showMediaControls = state.tmdbBuilderMode == TmdbBuilderMode.PRODUCTION ||
state.tmdbBuilderMode == TmdbBuilderMode.PERSON ||
state.tmdbBuilderMode == TmdbBuilderMode.DIRECTOR ||
state.tmdbBuilderMode == TmdbBuilderMode.DISCOVER
val showSortControls = state.tmdbBuilderMode == TmdbBuilderMode.PRODUCTION ||
state.tmdbBuilderMode == TmdbBuilderMode.NETWORK ||
state.tmdbBuilderMode == TmdbBuilderMode.PERSON ||
state.tmdbBuilderMode == TmdbBuilderMode.DIRECTOR ||
state.tmdbBuilderMode == TmdbBuilderMode.DISCOVER
val showFilterControls = state.tmdbBuilderMode == TmdbBuilderMode.DISCOVER
@ -1892,6 +1892,8 @@ private fun tmdbBuilderModeLabel(mode: TmdbBuilderMode): String =
TmdbBuilderMode.PRODUCTION -> stringResource(Res.string.collections_editor_tmdb_production_mode)
TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_network_mode)
TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_collection_mode)
TmdbBuilderMode.PERSON -> stringResource(Res.string.collections_editor_tmdb_person_mode)
TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_director_mode)
TmdbBuilderMode.DISCOVER -> stringResource(Res.string.collections_editor_tmdb_custom_mode)
}
@ -1903,6 +1905,8 @@ private fun tmdbModeHelpText(mode: TmdbBuilderMode): String =
TmdbBuilderMode.PRODUCTION -> stringResource(Res.string.collections_editor_tmdb_help_production)
TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_help_network)
TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_help_collection)
TmdbBuilderMode.PERSON -> stringResource(Res.string.collections_editor_tmdb_help_person)
TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_help_director)
TmdbBuilderMode.DISCOVER -> stringResource(Res.string.collections_editor_tmdb_help_discover)
}
@ -1913,6 +1917,8 @@ private fun tmdbInputLabel(mode: TmdbBuilderMode): String =
TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_network_id)
TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_collection_id)
TmdbBuilderMode.PRODUCTION -> stringResource(Res.string.collections_editor_tmdb_company_search)
TmdbBuilderMode.PERSON,
TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_person_id)
else -> stringResource(Res.string.collections_editor_tmdb_id_or_url)
}
@ -1923,6 +1929,8 @@ private fun tmdbInputPlaceholder(mode: TmdbBuilderMode): String =
TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_network_placeholder)
TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_collection_placeholder)
TmdbBuilderMode.PRODUCTION -> stringResource(Res.string.collections_editor_tmdb_company_placeholder)
TmdbBuilderMode.PERSON,
TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_person_placeholder)
else -> stringResource(Res.string.collections_editor_tmdb_id_or_url)
}
@ -1933,6 +1941,8 @@ private fun tmdbInputHelper(mode: TmdbBuilderMode): String =
TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_collection_helper)
TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_network_helper)
TmdbBuilderMode.LIST -> stringResource(Res.string.collections_editor_tmdb_list_helper)
TmdbBuilderMode.PERSON,
TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_person_helper)
else -> ""
}
@ -1940,12 +1950,15 @@ private fun tmdbInputHelper(mode: TmdbBuilderMode): String =
private fun tmdbTitlePlaceholder(mode: TmdbBuilderMode): String =
when (mode) {
TmdbBuilderMode.DISCOVER -> stringResource(Res.string.collections_editor_tmdb_discover_title_placeholder)
TmdbBuilderMode.PERSON -> stringResource(Res.string.collections_editor_tmdb_person_title_placeholder)
TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_director_title_placeholder)
else -> stringResource(Res.string.collections_editor_tmdb_title_placeholder)
}
@Composable
private fun tmdbSortLabel(sort: TmdbCollectionSort): String =
when (sort) {
TmdbCollectionSort.ORIGINAL -> stringResource(Res.string.collections_editor_tmdb_sort_original)
TmdbCollectionSort.POPULAR_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_popular)
TmdbCollectionSort.VOTE_AVERAGE_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_top_rated)
TmdbCollectionSort.RELEASE_DATE_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_recent)
@ -1979,6 +1992,16 @@ private fun tmdbSourceSubtitle(source: CollectionSource): String {
stringResource(Res.string.collections_editor_tmdb_series),
sort,
).joinToString("")
TmdbCollectionSourceType.PERSON -> listOf(
stringResource(Res.string.collections_editor_tmdb_subtitle_person),
media,
sort,
).joinToString("")
TmdbCollectionSourceType.DIRECTOR -> listOf(
stringResource(Res.string.collections_editor_tmdb_subtitle_director),
media,
sort,
).joinToString("")
TmdbCollectionSourceType.DISCOVER -> listOf(
stringResource(Res.string.collections_editor_tmdb_subtitle_discover),
media,

View file

@ -69,6 +69,8 @@ enum class TmdbCollectionSourceType {
COMPANY,
NETWORK,
DISCOVER,
PERSON,
DIRECTOR,
}
@Serializable
@ -86,6 +88,7 @@ enum class TmdbCollectionMediaType(val value: String) {
}
enum class TmdbCollectionSort(val value: String) {
ORIGINAL("original"),
POPULAR_DESC("popularity.desc"),
VOTE_AVERAGE_DESC("vote_average.desc"),
RELEASE_DATE_DESC("primary_release_date.desc"),
@ -133,6 +136,7 @@ data class CollectionFolder(
val sources: List<CollectionSource> = emptyList(),
val catalogSources: List<CollectionCatalogSource> = emptyList(),
val heroBackdropUrl: String? = null,
val heroVideoUrl: String? = null,
val titleLogoUrl: String? = null,
) {
val posterShape: PosterShape

View file

@ -4,7 +4,10 @@ import co.touchlab.kermit.Logger
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.addons.ManagedAddon
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
@ -33,6 +36,8 @@ object CollectionRepository {
private val _collections = MutableStateFlow<List<Collection>>(emptyList())
val collections: StateFlow<List<Collection>> = _collections.asStateFlow()
private val _localChangeEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
internal val localChangeEvents: SharedFlow<Unit> = _localChangeEvents.asSharedFlow()
private var rawCollectionsJson: JsonElement = JsonArray(emptyList())
private var hasLoaded = false
@ -244,16 +249,19 @@ object CollectionRepository {
internal fun applyFromRemote(collections: List<Collection>, rawJson: JsonElement) {
rawCollectionsJson = rawJson
_collections.value = collections
persist()
persist(sync = false)
}
private fun ensureLoaded() {
if (!hasLoaded) initialize()
}
private fun persist() {
private fun persist(sync: Boolean = true) {
runCatching {
CollectionStorage.savePayload(mergedCollectionsJson().toString())
if (sync) {
_localChangeEvents.tryEmit(Unit)
}
}.onFailure { e ->
log.e(e) { "Failed to persist collections" }
}

View file

@ -15,8 +15,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
@ -125,9 +123,7 @@ object CollectionSyncService {
@OptIn(FlowPreview::class)
private fun observeLocalChangesAndPush() {
observeJob = scope.launch {
CollectionRepository.collections
.drop(1)
.distinctUntilChanged()
CollectionRepository.localChangeEvents
.debounce(PUSH_DEBOUNCE_MS)
.collect {
if (isSyncingFromRemote) return@collect

View file

@ -140,16 +140,19 @@ object FolderDetailRepository {
source = source,
type = type,
catalogId = tmdbCatalogId(source),
supportsPagination = source.tmdbSourceType != TmdbCollectionSourceType.COLLECTION.name,
supportsPagination = source.tmdbSourceType !in setOf(
TmdbCollectionSourceType.COLLECTION.name,
TmdbCollectionSourceType.PERSON.name,
TmdbCollectionSourceType.DIRECTOR.name,
),
isLoading = true,
),
)
} else {
val catalogSource = source.addonCatalogSource() ?: return@forEach
val addon = addons.find { it.manifest?.id == catalogSource.addonId }
val catalog = addon?.manifest?.catalogs?.find {
it.id == catalogSource.catalogId && it.type == catalogSource.type
}
val resolvedCatalog = addons.findCollectionCatalog(catalogSource)
val addon = resolvedCatalog?.addon
val catalog = resolvedCatalog?.catalog
val label = catalog?.name ?: catalogSource.catalogId
val typeLabel = localizedMediaTypeLabel(catalogSource.type)
val genreSuffix = if (catalogSource.genre != null) " · ${catalogSource.genre}" else ""
@ -184,8 +187,8 @@ object FolderDetailRepository {
sources.forEachIndexed { sourceIndex, source ->
val tabIndex = if (showAll) sourceIndex + 1 else sourceIndex
val catalogSource = source.addonCatalogSource()
val addon = catalogSource?.let { value -> addons.find { it.manifest?.id == value.addonId } }
if (!source.isTmdb && addon == null) {
val resolvedCatalog = catalogSource?.let { addons.findCollectionCatalog(it) }
if (!source.isTmdb && resolvedCatalog == null) {
updateTab(tabIndex) {
it.copy(
isLoading = false,

View file

@ -29,6 +29,8 @@ object TmdbCollectionSourceResolver {
when (sourceType) {
TmdbCollectionSourceType.LIST -> resolveList(source, apiKey, language, page)
TmdbCollectionSourceType.COLLECTION -> resolveCollection(source, apiKey, language)
TmdbCollectionSourceType.PERSON,
TmdbCollectionSourceType.DIRECTOR -> resolvePersonCredits(source, apiKey, language)
TmdbCollectionSourceType.COMPANY,
TmdbCollectionSourceType.NETWORK,
TmdbCollectionSourceType.DISCOVER -> resolveDiscover(source, apiKey, language, page)
@ -85,6 +87,19 @@ object TmdbCollectionSourceResolver {
)
}
TmdbCollectionSourceType.PERSON,
TmdbCollectionSourceType.DIRECTOR -> {
val body = fetch<TmdbPersonResponse>(
endpoint = "person/$id",
apiKey = apiKey,
query = mapOf("language" to language),
) ?: error("TMDB person not found")
TmdbSourceImportMetadata(
title = body.name?.takeIf { it.isNotBlank() },
coverImageUrl = imageUrl(body.profilePath, "w500"),
)
}
TmdbCollectionSourceType.DISCOVER -> TmdbSourceImportMetadata(title = "TMDB Discover")
}
}
@ -153,7 +168,7 @@ object TmdbCollectionSourceResolver {
fun parseTmdbId(input: String): Int? {
val trimmed = input.trim()
trimmed.toIntOrNull()?.let { return it }
return Regex("""(?:list|collection|company|network)/(\d+)""")
return Regex("""(?:list|collection|company|network|person)/(\d+)""")
.find(trimmed)
?.groupValues
?.getOrNull(1)
@ -193,6 +208,7 @@ object TmdbCollectionSourceResolver {
) ?: error("TMDB list not found")
val items = body.items.orEmpty()
.mapNotNull { it.toPreview() }
.sortedFor(source.sortBy)
.distinctBy { "${it.type}:${it.id}" }
return CatalogPage(
items = items,
@ -213,12 +229,35 @@ object TmdbCollectionSourceResolver {
query = mapOf("language" to language),
) ?: error("TMDB collection not found")
val items = body.parts.orEmpty()
.sortedBy { it.releaseDate ?: "9999" }
.mapNotNull { it.toPreview(TmdbCollectionMediaType.MOVIE) }
.sortedFor(source.sortBy)
.distinctBy { it.id }
return CatalogPage(items = items, rawItemCount = items.size, nextSkip = null)
}
private suspend fun resolvePersonCredits(
source: CollectionSource,
apiKey: String,
language: String,
): CatalogPage {
val id = source.tmdbId ?: error("Missing TMDB person ID")
val mediaType = source.tmdbMediaType()
val body = fetch<TmdbPersonCreditsResponse>(
endpoint = "person/$id/combined_credits",
apiKey = apiKey,
query = mapOf("language" to language),
) ?: error("TMDB person credits not found")
val items = when (source.tmdbType()) {
TmdbCollectionSourceType.DIRECTOR -> body.crew.orEmpty()
.filter { it.job.equals("Director", ignoreCase = true) }
.mapNotNull { it.toPreview(mediaType) }
else -> body.cast.orEmpty().mapNotNull { it.toPreview(mediaType) }
}
.distinctBy { "${it.type}:${it.id}" }
.sortedFor(source.sortBy)
return CatalogPage(items = items, rawItemCount = items.size, nextSkip = null)
}
private suspend fun resolveDiscover(
source: CollectionSource,
apiKey: String,
@ -312,6 +351,21 @@ object TmdbCollectionSourceResolver {
}.getOrNull()
}
private fun List<MetaPreview>.sortedFor(sortBy: String?): List<MetaPreview> =
when (sortBy) {
TmdbCollectionSort.ORIGINAL.value -> this
TmdbCollectionSort.VOTE_AVERAGE_DESC.value -> sortedWith(
compareByDescending<MetaPreview> { it.imdbRating?.toDoubleOrNull() ?: -1.0 }
.thenByDescending { it.rawReleaseDate ?: it.releaseInfo.orEmpty() },
)
TmdbCollectionSort.RELEASE_DATE_DESC.value,
TmdbCollectionSort.FIRST_AIR_DATE_DESC.value -> sortedByDescending { it.rawReleaseDate ?: it.releaseInfo.orEmpty() }
TmdbCollectionSort.POPULAR_DESC.value,
null,
"" -> this
else -> this
}
private fun TmdbListItem.toPreview(): MetaPreview? {
val media = mediaType?.lowercase()
val contentType = if (media == "tv") TmdbCollectionMediaType.TV else TmdbCollectionMediaType.MOVIE
@ -362,6 +416,62 @@ object TmdbCollectionSourceResolver {
)
}
private fun TmdbPersonCreditCast.toPreview(mediaType: TmdbCollectionMediaType): MetaPreview? {
if (!matchesMediaType(mediaType, this.mediaType)) return null
val title = title?.takeIf { it.isNotBlank() }
?: name?.takeIf { it.isNotBlank() }
?: originalTitle?.takeIf { it.isNotBlank() }
?: originalName?.takeIf { it.isNotBlank() }
?: return null
return MetaPreview(
id = "tmdb:$id",
type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie",
name = title,
poster = imageUrl(posterPath, "w500") ?: imageUrl(backdropPath, "w780"),
banner = imageUrl(backdropPath, "w1280"),
posterShape = PosterShape.Poster,
description = overview?.takeIf { it.isNotBlank() },
releaseInfo = when (mediaType) {
TmdbCollectionMediaType.MOVIE -> releaseDate?.take(4)
TmdbCollectionMediaType.TV -> firstAirDate?.take(4)
},
rawReleaseDate = when (mediaType) {
TmdbCollectionMediaType.MOVIE -> releaseDate
TmdbCollectionMediaType.TV -> firstAirDate
},
popularity = popularity,
imdbRating = voteAverage?.let { ((it * 10).roundToInt() / 10.0).toString() },
)
}
private fun TmdbPersonCreditCrew.toPreview(mediaType: TmdbCollectionMediaType): MetaPreview? {
if (!matchesMediaType(mediaType, this.mediaType)) return null
val title = title?.takeIf { it.isNotBlank() }
?: name?.takeIf { it.isNotBlank() }
?: originalTitle?.takeIf { it.isNotBlank() }
?: originalName?.takeIf { it.isNotBlank() }
?: return null
return MetaPreview(
id = "tmdb:$id",
type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie",
name = title,
poster = imageUrl(posterPath, "w500") ?: imageUrl(backdropPath, "w780"),
banner = imageUrl(backdropPath, "w1280"),
posterShape = PosterShape.Poster,
description = overview?.takeIf { it.isNotBlank() },
releaseInfo = when (mediaType) {
TmdbCollectionMediaType.MOVIE -> releaseDate?.take(4)
TmdbCollectionMediaType.TV -> firstAirDate?.take(4)
},
rawReleaseDate = when (mediaType) {
TmdbCollectionMediaType.MOVIE -> releaseDate
TmdbCollectionMediaType.TV -> firstAirDate
},
popularity = popularity,
imdbRating = voteAverage?.let { ((it * 10).roundToInt() / 10.0).toString() },
)
}
private fun CollectionSource.tmdbType(): TmdbCollectionSourceType =
tmdbSourceType
?.let { raw -> runCatching { TmdbCollectionSourceType.valueOf(raw.uppercase()) }.getOrNull() }
@ -370,6 +480,12 @@ object TmdbCollectionSourceResolver {
private fun CollectionSource.tmdbMediaType(): TmdbCollectionMediaType =
TmdbCollectionMediaType.fromString(mediaType)
private fun matchesMediaType(expected: TmdbCollectionMediaType, actual: String?): Boolean =
when (expected) {
TmdbCollectionMediaType.MOVIE -> actual == "movie"
TmdbCollectionMediaType.TV -> actual == "tv"
}
private fun company(title: String, id: Int) = CollectionSource(
provider = "tmdb",
tmdbSourceType = TmdbCollectionSourceType.COMPANY.name,
@ -391,6 +507,7 @@ object TmdbCollectionSourceResolver {
private fun movieSort(sortBy: String?): String =
when (sortBy) {
TmdbCollectionSort.FIRST_AIR_DATE_DESC.value -> TmdbCollectionSort.RELEASE_DATE_DESC.value
TmdbCollectionSort.ORIGINAL.value -> TmdbCollectionSort.POPULAR_DESC.value
null, "" -> TmdbCollectionSort.POPULAR_DESC.value
else -> sortBy
}
@ -398,6 +515,7 @@ object TmdbCollectionSourceResolver {
private fun tvSort(sortBy: String?): String =
when (sortBy) {
TmdbCollectionSort.RELEASE_DATE_DESC.value -> TmdbCollectionSort.FIRST_AIR_DATE_DESC.value
TmdbCollectionSort.ORIGINAL.value -> TmdbCollectionSort.POPULAR_DESC.value
null, "" -> TmdbCollectionSort.POPULAR_DESC.value
else -> sortBy
}
@ -449,6 +567,12 @@ private data class TmdbNetworkResponse(
@SerialName("logo_path") val logoPath: String? = null,
)
@Serializable
private data class TmdbPersonResponse(
val name: String? = null,
@SerialName("profile_path") val profilePath: String? = null,
)
@Serializable
data class TmdbCompanySearchResult(
val id: Int,
@ -496,6 +620,47 @@ private data class TmdbGenreItem(
val name: String,
)
@Serializable
private data class TmdbPersonCreditsResponse(
val cast: List<TmdbPersonCreditCast>? = null,
val crew: List<TmdbPersonCreditCrew>? = null,
)
@Serializable
private data class TmdbPersonCreditCast(
val id: Int,
@SerialName("media_type") val mediaType: String? = null,
val title: String? = null,
val name: String? = null,
@SerialName("original_title") val originalTitle: String? = null,
@SerialName("original_name") val originalName: String? = null,
val overview: String? = null,
@SerialName("poster_path") val posterPath: String? = null,
@SerialName("backdrop_path") val backdropPath: String? = null,
@SerialName("release_date") val releaseDate: String? = null,
@SerialName("first_air_date") val firstAirDate: String? = null,
@SerialName("vote_average") val voteAverage: Double? = null,
val popularity: Double? = null,
)
@Serializable
private data class TmdbPersonCreditCrew(
val id: Int,
@SerialName("media_type") val mediaType: String? = null,
val title: String? = null,
val name: String? = null,
@SerialName("original_title") val originalTitle: String? = null,
@SerialName("original_name") val originalName: String? = null,
val overview: String? = null,
@SerialName("poster_path") val posterPath: String? = null,
@SerialName("backdrop_path") val backdropPath: String? = null,
@SerialName("release_date") val releaseDate: String? = null,
@SerialName("first_air_date") val firstAirDate: String? = null,
val job: String? = null,
@SerialName("vote_average") val voteAverage: Double? = null,
val popularity: Double? = null,
)
@Serializable
private data class TmdbListItem(
val id: Int,

View file

@ -365,7 +365,7 @@ private fun ContinueWatchingWideCard(
Text(
text = stringResource(
Res.string.home_continue_watching_watched,
continueWatchingProgressPercent(item.progressFraction),
"${continueWatchingProgressPercent(item.progressFraction)}%",
),
style = MaterialTheme.typography.labelSmall.copy(
fontSize = layout.progressLabelSize,

View file

@ -657,7 +657,6 @@ fun PlayerScreen(
}
}
playerController?.seekTo(targetPositionMs)
controlsVisible = true
showSeekFeedback(direction, nextState.amountMs)
accumulatedSeekResetJob?.cancel()

View file

@ -70,6 +70,7 @@ object ProfileRepository {
val stored = decodeStoredPayload() ?: return false
loadedCacheForUserId = stored.userId
applyStoredPayload(stored)
ThemeSettingsRepository.onProfileChanged()
return _state.value.profiles.isNotEmpty()
}

View file

@ -2,7 +2,9 @@ package com.nuvio.app.features.settings
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.lang_english
import nuvio.composeapp.generated.resources.lang_french
import nuvio.composeapp.generated.resources.lang_spanish
import nuvio.composeapp.generated.resources.lang_portuguese_portugal
import nuvio.composeapp.generated.resources.lang_turkish
import nuvio.composeapp.generated.resources.lang_italian
import nuvio.composeapp.generated.resources.lang_greek
@ -14,6 +16,7 @@ enum class AppLanguage(
val labelRes: StringResource,
) {
ENGLISH("en", Res.string.lang_english),
FRENCH("fr", Res.string.lang_french),
SPANISH("es", Res.string.lang_spanish),
TURKISH("tr", Res.string.lang_turkish),
ITALIAN("it", Res.string.lang_italian),

View file

@ -14,7 +14,8 @@ actual object ThemeSettingsStorage {
private const val selectedThemeKey = "selected_theme"
private const val amoledEnabledKey = "amoled_enabled"
private const val selectedAppLanguageKey = "selected_app_language"
private val syncKeys = listOf(selectedThemeKey, amoledEnabledKey, selectedAppLanguageKey)
private val profileScopedSyncKeys = listOf(selectedThemeKey, amoledEnabledKey)
private val globalSyncKeys = listOf(selectedAppLanguageKey)
actual fun loadSelectedTheme(): String? =
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(selectedThemeKey))
@ -37,11 +38,16 @@ actual object ThemeSettingsStorage {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(amoledEnabledKey))
}
actual fun loadSelectedAppLanguage(): String? =
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(selectedAppLanguageKey))
actual fun loadSelectedAppLanguage(): String? {
val value = NSUserDefaults.standardUserDefaults.stringForKey(selectedAppLanguageKey)
if (value != null) return value
val legacy = NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(selectedAppLanguageKey))
if (legacy != null) saveSelectedAppLanguage(legacy)
return legacy
}
actual fun saveSelectedAppLanguage(languageCode: String) {
NSUserDefaults.standardUserDefaults.setObject(languageCode, forKey = ProfileScopedKey.of(selectedAppLanguageKey))
NSUserDefaults.standardUserDefaults.setObject(languageCode, forKey = selectedAppLanguageKey)
}
actual fun applySelectedAppLanguage(languageCode: String) = Unit
@ -53,9 +59,12 @@ actual object ThemeSettingsStorage {
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
syncKeys.forEach { key ->
profileScopedSyncKeys.forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key))
}
globalSyncKeys.forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(key)
}
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)

View file

@ -22,7 +22,7 @@ ktor = "3.4.1"
material3 = "1.11.0-alpha07"
androidx-media3 = "1.8.0"
supabase = "3.4.1"
quickjsKt = "1.0.1"
quickjsKt = "1.0.5"
ksoup = "0.2.6"
reorderable = "3.0.0"
desugarJdkLibs = "2.1.5"

View file

@ -1,3 +1,3 @@
CURRENT_PROJECT_VERSION=40
MARKETING_VERSION=0.1.9
CURRENT_PROJECT_VERSION=42
MARKETING_VERSION=0.1.10

@ -1 +0,0 @@
Subproject commit df33966d7fbc6eb14e43fb1892e062417d76e7f5

@ -1 +0,0 @@
Subproject commit 8a8ddddf430555878273da13006fc57e182b0c0c