Merge branch 'cmp-rewrite' into trailer-fullscreen-player

This commit is contained in:
Marius Butz 2026-05-05 14:04:00 +02:00
commit 7c941fefe3
23 changed files with 1991 additions and 501 deletions

View file

@ -0,0 +1,91 @@
package com.nuvio.app.features.player
import android.content.res.Resources
import androidx.media3.common.Format
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.DefaultTrackNameProvider
@UnstableApi
class CustomDefaultTrackNameProvider(resources: Resources) : DefaultTrackNameProvider(resources) {
override fun getTrackName(format: Format): String {
var trackName = super.getTrackName(format)
if (format.sampleMimeType != null) {
var sampleFormat = formatNameFromMime(format.sampleMimeType)
if (sampleFormat == null) {
sampleFormat = formatNameFromMime(format.codecs)
}
if (sampleFormat == null) {
sampleFormat = format.sampleMimeType
}
if (sampleFormat != null) {
trackName += " ($sampleFormat)"
}
}
if (format.label != null) {
if (!trackName.startsWith(format.label!!)) {
trackName += " - ${format.label}"
}
}
return trackName
}
companion object {
fun formatNameFromMime(mimeType: String?): String? {
if (mimeType == null) return null
return when (mimeType) {
MimeTypes.AUDIO_DTS -> "DTS"
MimeTypes.AUDIO_DTS_HD -> "DTS-HD"
MimeTypes.AUDIO_DTS_EXPRESS -> "DTS Express"
MimeTypes.AUDIO_TRUEHD -> "TrueHD"
MimeTypes.AUDIO_AC3 -> "AC-3"
MimeTypes.AUDIO_E_AC3 -> "E-AC-3"
MimeTypes.AUDIO_E_AC3_JOC -> "E-AC-3-JOC"
MimeTypes.AUDIO_AC4 -> "AC-4"
MimeTypes.AUDIO_AAC -> "AAC"
MimeTypes.AUDIO_MPEG -> "MP3"
MimeTypes.AUDIO_MPEG_L2 -> "MP2"
MimeTypes.AUDIO_VORBIS -> "Vorbis"
MimeTypes.AUDIO_OPUS -> "Opus"
MimeTypes.AUDIO_FLAC -> "FLAC"
MimeTypes.AUDIO_ALAC -> "ALAC"
MimeTypes.AUDIO_WAV -> "WAV"
MimeTypes.AUDIO_AMR -> "AMR"
MimeTypes.AUDIO_AMR_NB -> "AMR-NB"
MimeTypes.AUDIO_AMR_WB -> "AMR-WB"
MimeTypes.AUDIO_IAMF -> "IAMF"
MimeTypes.AUDIO_MPEGH_MHA1 -> "MPEG-H"
MimeTypes.AUDIO_MPEGH_MHM1 -> "MPEG-H"
MimeTypes.VIDEO_H264 -> "AVC"
MimeTypes.VIDEO_H265 -> "HEVC"
MimeTypes.VIDEO_AV1 -> "AV1"
MimeTypes.VIDEO_VP8 -> "VP8"
MimeTypes.VIDEO_VP9 -> "VP9"
MimeTypes.VIDEO_DOLBY_VISION -> "Dolby Vision"
"application/pgs" -> "PGS"
MimeTypes.APPLICATION_SUBRIP -> "SRT"
MimeTypes.TEXT_SSA -> "SSA"
MimeTypes.TEXT_VTT -> "VTT"
MimeTypes.APPLICATION_TTML -> "TTML"
MimeTypes.APPLICATION_TX3G -> "TX3G"
MimeTypes.APPLICATION_DVBSUBS -> "DVB"
else -> null
}
}
fun getChannelLayoutName(channelCount: Int): String? {
return when (channelCount) {
1 -> "Mono"
2 -> "Stereo"
6 -> "5.1"
8 -> "7.1"
else -> if (channelCount > 0) "${channelCount}ch" else null
}
}
}
}

View file

@ -58,7 +58,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL
import java.util.Locale
private const val TAG = "NuvioPlayer"
@ -180,6 +179,10 @@ actual fun PlatformPlayerSurface(
var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) }
var subtitleSelectionJob by remember { mutableStateOf<Job?>(null) }
fun syncPlayerViewKeepScreenOn() {
playerViewRef?.keepScreenOn = exoPlayer.shouldKeepPlayerScreenOn()
}
DisposableEffect(exoPlayer) {
PlayerPictureInPictureManager.registerPausePlaybackCallback {
exoPlayer.pause()
@ -187,6 +190,7 @@ actual fun PlatformPlayerSurface(
val listener = object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
syncPlayerViewKeepScreenOn()
latestOnError.value(error.localizedMessage ?: runBlocking { getString(Res.string.player_unable_to_play_stream) })
}
@ -203,10 +207,12 @@ actual fun PlatformPlayerSurface(
latestOnError.value(null)
exoPlayer.logCurrentTracks("STATE_READY")
}
syncPlayerViewKeepScreenOn()
latestOnSnapshot.value(exoPlayer.snapshot())
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
syncPlayerViewKeepScreenOn()
latestOnSnapshot.value(exoPlayer.snapshot())
}
@ -236,6 +242,7 @@ actual fun PlatformPlayerSurface(
onDispose {
PlayerPictureInPictureManager.registerPausePlaybackCallback(null)
exoPlayer.removeListener(listener)
playerViewRef?.keepScreenOn = false
subtitleSelectionJob?.cancel()
}
}
@ -265,6 +272,7 @@ actual fun PlatformPlayerSurface(
LaunchedEffect(exoPlayer, playWhenReady) {
exoPlayer.playWhenReady = playWhenReady
syncPlayerViewKeepScreenOn()
latestOnSnapshot.value(exoPlayer.snapshot())
}
@ -298,10 +306,10 @@ actual fun PlatformPlayerSurface(
}
override fun getAudioTracks(): List<AudioTrack> =
exoPlayer.extractAudioTracks()
exoPlayer.extractAudioTracks(context)
override fun getSubtitleTracks(): List<SubtitleTrack> {
val tracks = exoPlayer.extractSubtitleTracks()
val tracks = exoPlayer.extractSubtitleTracks(context)
Log.d(TAG, "getSubtitleTracks: found ${tracks.size} tracks")
tracks.forEach { t ->
Log.d(TAG, " track idx=${t.index} id=${t.id} label='${t.label}' lang=${t.language} selected=${t.isSelected}")
@ -425,7 +433,7 @@ actual fun PlatformPlayerSurface(
useController = useNativeController
layoutParams = android.view.ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
player = exoPlayer
keepScreenOn = true
keepScreenOn = exoPlayer.shouldKeepPlayerScreenOn()
this.resizeMode = resizeMode.toExoResizeMode()
setShutterBackgroundColor(android.graphics.Color.BLACK)
playerViewRef = this
@ -442,6 +450,7 @@ actual fun PlatformPlayerSurface(
playerView.useController = useNativeController
playerView.resizeMode = resizeMode.toExoResizeMode()
playerViewRef = playerView
syncPlayerViewKeepScreenOn()
playerView.syncLibassOverlay(
player = exoPlayer,
enabled = useLibass,
@ -470,6 +479,11 @@ private fun ExoPlayer.snapshot(): PlayerPlaybackSnapshot =
playbackSpeed = playbackParameters.speed,
)
private fun ExoPlayer.shouldKeepPlayerScreenOn(): Boolean =
playerError == null &&
playWhenReady &&
playbackState in setOf(Player.STATE_BUFFERING, Player.STATE_READY)
private fun PlayerResizeMode.toExoResizeMode(): Int =
when (this) {
PlayerResizeMode.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT
@ -559,47 +573,20 @@ private fun PlayerView.applySubtitleStyle(style: SubtitleStyleState) {
}
}
private fun ExoPlayer.extractAudioTracks(): List<AudioTrack> {
private fun ExoPlayer.extractAudioTracks(context: Context): List<AudioTrack> {
val tracks = mutableListOf<AudioTrack>()
val trackNameProvider = CustomDefaultTrackNameProvider(context.resources)
var idx = 0
for (group in currentTracks.groups) {
if (group.type != C.TRACK_TYPE_AUDIO) continue
val format = group.mediaTrackGroup.getFormat(0)
val channelLabel = when {
format.channelCount == 1 -> "Mono"
format.channelCount == 2 -> "Stereo"
format.channelCount == 6 -> "5.1"
format.channelCount == 8 -> "7.1"
format.channelCount > 0 -> "${format.channelCount}ch"
else -> null
}
val mime = format.sampleMimeType?.lowercase()
val codecLabel = when {
mime == null -> null
mime.contains("eac3-joc") -> "Dolby Atmos"
mime.contains("truehd") && format.channelCount >= 8 -> "Dolby Atmos"
mime.contains("truehd") -> "Dolby TrueHD"
mime.contains("eac3") -> "Dolby Digital Plus"
mime.contains("ac3") -> "Dolby Digital"
mime.contains("opus") -> "Opus"
mime.contains("aac") -> "AAC"
mime.contains("dts-hd") -> "DTS-HD"
mime.contains("dts") -> "DTS"
else -> null
}
val resolvedLanguage = format.language?.let { lang -> Locale(lang).displayLanguage.takeIf { name -> name.isNotBlank() && name != lang } }
val baseName = format.label?.takeIf { it.isNotBlank() }
?: resolvedLanguage
?: format.language
val label = trackNameProvider.getTrackName(format).takeIf { it.isNotBlank() }
?: runBlocking { getString(Res.string.compose_player_track_number, idx + 1) }
val suffix = listOfNotNull(channelLabel, codecLabel)
.joinToString(" ")
.let { if (it.isNotBlank()) " ($it)" else "" }
tracks.add(
AudioTrack(
index = idx,
id = format.id ?: idx.toString(),
label = "$baseName$suffix",
label = label,
language = format.language,
isSelected = group.isSelected,
)
@ -609,8 +596,9 @@ private fun ExoPlayer.extractAudioTracks(): List<AudioTrack> {
return tracks
}
private fun ExoPlayer.extractSubtitleTracks(): List<SubtitleTrack> {
private fun ExoPlayer.extractSubtitleTracks(context: Context): List<SubtitleTrack> {
val tracks = mutableListOf<SubtitleTrack>()
val trackNameProvider = CustomDefaultTrackNameProvider(context.resources)
var idx = 0
for (group in currentTracks.groups) {
if (group.type != C.TRACK_TYPE_TEXT) continue
@ -620,7 +608,7 @@ private fun ExoPlayer.extractSubtitleTracks(): List<SubtitleTrack> {
SubtitleTrack(
index = idx,
id = format.id ?: idx.toString(),
label = format.label ?: "",
label = trackNameProvider.getTrackName(format),
language = format.language,
isSelected = group.isSelected,
isForced = inferForcedSubtitleTrack(

View file

@ -35,7 +35,7 @@ actual fun LockPlayerToLandscape() {
}
@Composable
actual fun EnterImmersivePlayerMode() {
actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) {
val activity = LocalContext.current.findActivity() ?: return
DisposableEffect(activity) {

View file

@ -8,4 +8,5 @@
<locale android:name="it"/>
<locale android:name="el"/>
<locale android:name="pl"/>
<locale android:name="de"/>
</locale-config>

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_supporters_contributors_subtitle">Reconnaissance et crédits du projet</string>
<string name="action_back">Retour</string>
@ -111,29 +110,38 @@
<string name="collections_editor_tmdb_production_mode">Production</string>
<string name="collections_editor_tmdb_network_mode">Chaîne</string>
<string name="collections_editor_tmdb_collection_mode">Collection</string>
<string name="collections_editor_tmdb_person_mode">Personne</string>
<string name="collections_editor_tmdb_director_mode">Réalisateur</string>
<string name="collections_editor_tmdb_custom_mode">Personnalisé</string>
<string name="collections_editor_tmdb_help_presets">Choisissez une source prédéfinie. Vous pouvez la modifier ou la supprimer après l'avoir ajoutée.</string>
<string name="collections_editor_tmdb_help_list">Collez une URL de liste publique TMDB ou uniquement le numéro de l'URL.</string>
<string name="collections_editor_tmdb_help_production">Recherchez par nom de studio, ou collez un ID/URL de société TMDB et ajoutez-le directement.</string>
<string name="collections_editor_tmdb_help_network">Saisissez un ID de chaîne. Les chaînes courantes sont disponibles dans les préréglages et les filtres rapides.</string>
<string name="collections_editor_tmdb_help_collection">Recherchez le nom d'une collection de films ou collez l'ID de collection TMDB.</string>
<string name="collections_editor_tmdb_help_person">Saisissez un ID ou une URL de personne TMDB pour créer une ligne à partir des crédits de casting.</string>
<string name="collections_editor_tmdb_help_director">Saisissez un ID ou une URL de personne TMDB pour créer une ligne à partir des crédits de réalisation.</string>
<string name="collections_editor_tmdb_help_discover">Créez une ligne TMDB dynamique avec des filtres optionnels. Laissez les champs vides si vous n'avez pas besoin de ce filtre.</string>
<string name="collections_editor_tmdb_public_list">Liste publique TMDB</string>
<string name="collections_editor_tmdb_network_id">ID de chaîne</string>
<string name="collections_editor_tmdb_collection_id">ID de collection</string>
<string name="collections_editor_tmdb_person_id">ID de personne</string>
<string name="collections_editor_tmdb_company_search">Nom, ID ou URL de société de production</string>
<string name="collections_editor_tmdb_id_or_url">ID ou URL TMDB</string>
<string name="collections_editor_tmdb_list_placeholder">https://www.themoviedb.org/list/8504994 ou 8504994</string>
<string name="collections_editor_tmdb_network_placeholder">213 pour Netflix, 49 pour HBO, 2739 pour Disney+</string>
<string name="collections_editor_tmdb_collection_placeholder">10 pour Star Wars Collection</string>
<string name="collections_editor_tmdb_company_placeholder">Marvel Studios, 420 ou URL de société</string>
<string name="collections_editor_tmdb_person_placeholder">31 pour Tom Hanks, ou URL de personne</string>
<string name="collections_editor_tmdb_search_helper">Exemples : Marvel Studios, 420 ou https://www.themoviedb.org/company/420.</string>
<string name="collections_editor_tmdb_collection_helper">Exemple : Star Wars Collection, Harry Potter Collection ou une URL de collection.</string>
<string name="collections_editor_tmdb_network_helper">Exemples d'ID : Netflix 213, HBO 49, Disney+ 2739.</string>
<string name="collections_editor_tmdb_list_helper">Exemple : https://www.themoviedb.org/list/8504994 ou 8504994.</string>
<string name="collections_editor_tmdb_person_helper">Exemple : https://www.themoviedb.org/person/31-tom-hanks ou 31.</string>
<string name="collections_editor_tmdb_display_title">Titre affiché</string>
<string name="collections_editor_tmdb_title_helper">Affiché comme nom de ligne/onglet. Si vide, Nuvio en génère un depuis la source.</string>
<string name="collections_editor_tmdb_title_placeholder">Films Marvel, Originaux Netflix, Pixar</string>
<string name="collections_editor_tmdb_person_title_placeholder">Films avec Tom Hanks, Acteurs favoris</string>
<string name="collections_editor_tmdb_director_title_placeholder">Films de Christopher Nolan, Réalisateurs favoris</string>
<string name="collections_editor_tmdb_discover_title_placeholder">Meilleurs films d'action, drames coréens, animation 2024</string>
<string name="collections_editor_tmdb_search_results">Résultats de recherche</string>
<string name="collections_editor_tmdb_collection">Collection TMDB</string>
@ -180,6 +188,27 @@
<string name="collections_editor_tmdb_presets">Préréglages</string>
<string name="collections_editor_tmdb_search">Rechercher</string>
<string name="collections_editor_add_source">Ajouter une source</string>
<string name="collections_editor_add_trakt_source">Ajouter une liste Trakt</string>
<string name="collections_editor_edit_trakt_source">Modifier la liste Trakt</string>
<string name="collections_editor_trakt_sources">Listes Trakt</string>
<string name="collections_editor_trakt_list">Liste Trakt</string>
<string name="collections_editor_trakt_input_placeholder">Rechercher un titre, URL Trakt ou ID de liste</string>
<string name="collections_editor_trakt_input_helper">Utilisez une URL publique de liste Trakt ou un ID numérique de liste, ou recherchez par nom.</string>
<string name="collections_editor_trakt_title_placeholder">Programme du week-end, Lauréats</string>
<string name="collections_editor_trakt_search_results">Résultats de recherche</string>
<string name="collections_editor_trakt_trending">Listes tendances</string>
<string name="collections_editor_trakt_popular">Listes populaires</string>
<string name="collections_editor_trakt_direction">Ordre</string>
<string name="collections_editor_trakt_ascending">Croissant</string>
<string name="collections_editor_trakt_descending">Décroissant</string>
<string name="collections_editor_trakt_sort_list_order">Ordre de la liste</string>
<string name="collections_editor_trakt_sort_recently_added">Ajoutés récemment</string>
<string name="collections_editor_trakt_sort_title">Titre</string>
<string name="collections_editor_trakt_sort_released">Date de sortie</string>
<string name="collections_editor_trakt_sort_runtime">Durée</string>
<string name="collections_editor_trakt_sort_popular">Populaire</string>
<string name="collections_editor_trakt_sort_percentage">Pourcentage</string>
<string name="collections_editor_trakt_sort_votes">Votes</string>
<string name="collections_editor_tmdb_genre_action">Action</string>
<string name="collections_editor_tmdb_genre_adventure">Aventure</string>
<string name="collections_editor_tmdb_genre_animation">Animation</string>
@ -213,6 +242,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">Populaire</string>
<string name="collections_editor_tmdb_sort_top_rated">Mieux notés</string>
<string name="collections_editor_tmdb_sort_recent">Récent</string>
@ -220,6 +250,8 @@
<string name="collections_editor_tmdb_subtitle_movie_collection">Collection de films TMDB</string>
<string name="collections_editor_tmdb_subtitle_production">Production</string>
<string name="collections_editor_tmdb_subtitle_network">Chaîne</string>
<string name="collections_editor_tmdb_subtitle_person">Personne</string>
<string name="collections_editor_tmdb_subtitle_director">Réalisateur</string>
<string name="collections_editor_tmdb_subtitle_discover">Découverte TMDB</string>
<string name="collections_empty_subtitle">Créez-en une pour organiser vos catalogues.</string>
<string name="collections_empty_title">Aucune collection</string>
@ -251,7 +283,7 @@
<string name="compose_auth_sign_in_subtitle">Connectez-vous pour accéder à votre bibliothèque et votre progression</string>
<string name="compose_auth_sign_in">Se connecter</string>
<string name="compose_auth_sign_up_subtitle">Inscrivez-vous pour synchroniser vos données entre appareils</string>
<string name="compose_auth_sign_up">S\'inscrire</string>
<string name="compose_auth_sign_up">S'inscrire</string>
<string name="compose_auth_store_locally">Vos données seront uniquement stockées localement</string>
<string name="compose_auth_tagline">Regardez tout, partout</string>
<string name="compose_auth_welcome_back">Bon retour</string>
@ -314,11 +346,11 @@
<string name="compose_profile_add_profile">Ajouter un profil</string>
<string name="compose_search_clear">Effacer la recherche</string>
<string name="compose_search_discover_title">Découvrir</string>
<string name="compose_search_empty_failed_message">Les addons installés n'ont retourné aucun résultat de recherche valide.</string>
<string name="compose_search_empty_failed_message">Les addons installés n'ont renvoyé aucun résultat de recherche valide.</string>
<string name="compose_search_empty_failed_title">La recherche a échoué</string>
<string name="compose_search_empty_no_active_addons_message">Installez et validez au moins un addon avant de rechercher.</string>
<string name="compose_search_empty_no_active_addons_title">Aucun addon active</string>
<string name="compose_search_empty_no_results_message">Les catalogues installés n'ont retourné aucun résultat pour cette requête.</string>
<string name="compose_search_empty_no_active_addons_title">Aucun addon actif</string>
<string name="compose_search_empty_no_results_message">Les catalogues installés n'ont renvoyé aucun résultat pour cette requête.</string>
<string name="compose_search_empty_no_results_title">Aucun résultat trouvé</string>
<string name="compose_search_empty_no_search_catalogs_message">Vos addons installés n'exposent pas de catalogue de recherche.</string>
<string name="compose_search_empty_no_search_catalogs_title">Aucun catalogue de recherche</string>
@ -448,7 +480,7 @@
<string name="settings_homescreen_visible">Visible</string>
<string name="settings_playback_subtitle">Lecteur, sous-titres et lecture automatique</string>
<string name="settings_poster_card_radius">Rayon de carte</string>
<string name="settings_poster_card_style">STYLE DE CARTE D\'AFFICHE</string>
<string name="settings_poster_card_style">STYLE DE CARTE D'AFFICHE</string>
<string name="settings_poster_card_width">Largeur de carte</string>
<string name="settings_poster_custom">Personnalisé</string>
<string name="settings_poster_description">Personnalisez la largeur de carte et le rayon des coins pour les cartes d'affiches partagées dans toute l'application.</string>
@ -552,7 +584,7 @@
<string name="settings_notifications_test_title">Notification de test</string>
<string name="community_section_title">Communauté</string>
<string name="community_section_description">Découvrez les personnes qui construisent et soutiennent Nuvio sur Mobile, TV et Web.</string>
<string name="community_supporters_not_configured">L\'API des supporters n'est pas configurée. Ajoutez DONATIONS_BASE_URL dans local.properties.</string>
<string name="community_supporters_not_configured">L'API des supporters n'est pas configurée. Ajoutez DONATIONS_BASE_URL dans local.properties.</string>
<string name="community_tab_contributors">Contributeurs</string>
<string name="community_tab_supporters">Supporters</string>
<string name="community_open_github">Ouvrir GitHub</string>
@ -582,7 +614,7 @@
<string name="community_month_nov">Nov</string>
<string name="community_month_dec">Déc</string>
<string name="community_date_format">%1$s %2$s %3$s</string>
<string name="settings_playback_all_addons">Toutes les addons</string>
<string name="settings_playback_all_addons">Tous les addons</string>
<string name="settings_playback_all_plugins">Tous les plugins</string>
<string name="settings_playback_allowed_addons">Addons autorisés</string>
<string name="settings_playback_allowed_plugins">Plugins autorisés</string>
@ -628,7 +660,7 @@
<string name="settings_playback_regex_matches_against">Correspond au nom du stream, à l'étiquette, à la description, à l'addon et à l'URL.</string>
<string name="settings_playback_regex_pattern">Modèle regex</string>
<string name="settings_playback_regex_placeholder">4K|2160p|Remux</string>
<string name="settings_playback_regex_preset_any_1080p">N\'importe quel 1080p+</string>
<string name="settings_playback_regex_preset_any_1080p">N'importe quel 1080p+</string>
<string name="settings_playback_regex_preset_avc_x264">AVC / x264</string>
<string name="settings_playback_regex_preset_bluray_quality">Qualité BluRay</string>
<string name="settings_playback_regex_preset_dolby_atmos_dts">Dolby Atmos / DTS</string>
@ -665,8 +697,8 @@
<string name="settings_playback_skip_intro_outro_recap">Passer l'intro/outro/récap</string>
<string name="settings_playback_skip_intro_outro_recap_description">Afficher un bouton de saut lors des segments d'intro, d'outro et de récapitulatif détectés.</string>
<string name="settings_playback_source_scope">Périmètre des sources</string>
<string name="settings_playback_source_scope_all_addons">Toutes les addons</string>
<string name="settings_playback_source_scope_all_addons_description">Considérer les streams de toutes les addons installés.</string>
<string name="settings_playback_source_scope_all_addons">Tous les addons</string>
<string name="settings_playback_source_scope_all_addons_description">Considérer les streams de tous les addons installés.</string>
<string name="settings_playback_source_scope_all_sources">Toutes les sources</string>
<string name="settings_playback_source_scope_all_sources_description">Considérer les streams des addons et des plugins.</string>
<string name="settings_playback_source_scope_enabled_plugins_only">Plugins activés uniquement</string>
@ -858,14 +890,14 @@
<string name="action_yes">Oui</string>
<string name="app_exit_message">Voulez-vous quitter l'application ?</string>
<string name="app_exit_title">Quitter l'application</string>
<string name="catalog_empty_message">Ce catalogue n'a retourné aucun élément.</string>
<string name="catalog_empty_message">Ce catalogue n'a renvoyé aucun élément.</string>
<string name="catalog_empty_title">Aucun titre trouvé</string>
<string name="details_check_connection">Vérifiez votre connexion WiFi ou données mobiles et réessayez.</string>
<string name="details_director">Réalisateur</string>
<string name="details_failed_to_load">Échec du chargement</string>
<string name="details_more_like_this">Plus comme ceci</string>
<string name="details_seasons">Saisons</string>
<string name="details_series_missing_numbers">Cet addon a retourné des vidéos pour la série, mais aucune n'incluait de numéros de saison ou d'épisode.</string>
<string name="details_series_missing_numbers">Cet addon a renvoyé des vidéos pour la série, mais aucune n'incluait de numéros de saison ou d'épisode.</string>
<string name="details_series_no_metadata">Cet addon n'a fourni aucune métadonnée d'épisode pour cette série.</string>
<string name="details_series_unpublished">Cet addon n'a pas encore publié d'épisodes.</string>
<string name="details_servers_unreachable">Votre appareil est en ligne, mais Nuvio n'a pas pu se connecter aux serveurs nécessaires.</string>
@ -875,11 +907,11 @@
<string name="discover_all_genres">Tous les genres</string>
<string name="discover_catalog">Catalogue</string>
<string name="discover_catalog_context">%1$s • %2$s</string>
<string name="discover_empty_load_failed_message">Le catalogue sélectionné n'a retourné aucun élément de découverte.</string>
<string name="discover_empty_load_failed_message">Le catalogue sélectionné n'a renvoyé aucun élément de découverte.</string>
<string name="discover_empty_load_failed_title">Impossible de charger Découvrir</string>
<string name="discover_empty_no_catalogs_message">Les addons installés n'exposent pas de catalogues compatibles avec le tableau pour Découvrir.</string>
<string name="discover_empty_no_catalogs_title">Aucun catalogue de découverte</string>
<string name="discover_empty_no_results_message">Le catalogue et les filtres sélectionnés n'ont retourné aucun élément.</string>
<string name="discover_empty_no_results_message">Le catalogue et les filtres sélectionnés n'ont renvoyé aucun élément.</string>
<string name="discover_empty_no_results_title">Aucun titre trouvé</string>
<string name="discover_empty_no_active_addons_message">Installez et validez au moins un addon avant d'explorer les catalogues dans Découvrir.</string>
<string name="discover_select_catalog">Sélectionner un catalogue</string>
@ -960,7 +992,7 @@
<string name="profile_select_avatar">Sélectionnez un avatar pour ce profil.</string>
<string name="profile_set_pin_lock">Configurer le verrouillage PIN</string>
<string name="profile_unnamed">Profil sans nom</string>
<string name="profile_use_primary_addons">Utiliser les addons principales</string>
<string name="profile_use_primary_addons">Utiliser les addons principaux</string>
<string name="profile_use_primary_addons_description">Partager la configuration des addons du profil principal plutôt que de gérer une liste séparée.</string>
<string name="profile_who_is_watching">Qui regarde ?</string>
<string name="provider_downloaded">Téléchargé</string>
@ -969,12 +1001,12 @@
<string name="streams_checking_more_addons">Vérification d'autres addons…</string>
<string name="streams_copy_link">Copier le lien du stream</string>
<string name="streams_download_file">Télécharger le fichier</string>
<string name="streams_empty_load_failed_message">Les addons de streams installés n'ont pas retourné de réponse valide.</string>
<string name="streams_empty_load_failed_message">Les addons de streams installés n'ont pas renvoyé de réponse valide.</string>
<string name="streams_empty_load_failed_title">Impossible de charger les streams</string>
<string name="streams_empty_no_addons_message">Installez d'abord un addon pour charger les streams de ce titre.</string>
<string name="streams_empty_no_stream_addon_message">Vos addons installés ne fournissent pas de streams pour ce type de titre.</string>
<string name="streams_empty_no_stream_addon_title">Aucun addon de streams disponible</string>
<string name="streams_empty_no_streams_message">Aucune de vos addons installés n'a retourné de streams pour ce titre.</string>
<string name="streams_empty_no_streams_message">Aucun de vos addons installés n'a renvoyé de stream pour ce titre.</string>
<string name="streams_episode_badge">S%1$d E%2$d</string>
<string name="streams_episode_fallback_title">Épisode</string>
<string name="streams_episode_title_with_name">S%1$dE%2$d - %3$s</string>
@ -1047,7 +1079,7 @@
<string name="profile_pin_offline_verification_requires_online">Ce code PIN ne peut pas encore être vérifié hors ligne sur cet appareil. Connectez-vous une fois et déverrouillez-le en ligne d'abord.</string>
<string name="profile_pin_set_failed">Impossible de définir le code PIN. Veuillez réessayer.</string>
<string name="profile_pin_set_requires_internet">Connectez-vous à Internet pour définir un code PIN.</string>
<string name="profile_primary_addons_required">Ce profil utilise les addons principales.</string>
<string name="profile_primary_addons_required">Ce profil utilise les addons principaux.</string>
<string name="streams_failed_to_load_scraper">Impossible de charger %1$s</string>
<string name="stream_default_name">Source</string>
<string name="source_embedded">Intégré</string>
@ -1058,7 +1090,7 @@
<string name="trakt_invalid_token_response">Réponse de jeton Trakt invalide</string>
<string name="trakt_library_load_failed">Impossible de charger la bibliothèque Trakt</string>
<string name="trakt_list_fallback_title">Liste %1$d</string>
<string name="trakt_missing_auth_code">Trakt n'a pas retourné de code d'autorisation</string>
<string name="trakt_missing_auth_code">Trakt n'a pas renvoyé de code d'autorisation</string>
<string name="trakt_missing_credentials">Identifiants Trakt manquants</string>
<string name="trakt_progress_load_failed">Impossible de charger la progression Trakt</string>
<string name="trakt_sign_in_complete_failed">Impossible de terminer la connexion Trakt</string>
@ -1071,11 +1103,12 @@
<string name="action_play_episode">Lire %1$s</string>
<string name="action_resume_episode">Reprendre %1$s</string>
<string name="collections_import_error_empty_json">Le JSON est vide.</string>
<string name="collections_import_error_collection_blank_id">La collection %1$d a un ID vide.</string>
<string name="collections_import_error_collection_blank_title">La collection \'%1$s' a un titre vide.</string>
<string name="collections_import_error_folder_blank_id">Le dossier %1$d dans \'%2$s' a un ID vide.</string>
<string name="collections_import_error_folder_blank_title">Le dossier \'%1$s\' dans \'%2$s\' a un titre vide.</string>
<string name="collections_import_error_source_blank_fields">La source %1$d dans le dossier \'%2$s\' a des champs vides.</string>
<string name="collections_import_error_collection_blank_id">La collection '%1$d' a un ID vide.</string>
<string name="collections_import_error_collection_blank_title">La collection '%1$s' a un titre vide.</string>
<string name="collections_import_error_folder_blank_id">Le dossier '%1$d' dans '%2$s' a un ID vide.</string>
<string name="collections_import_error_folder_blank_title">Le dossier '%1$s' dans '%2$s' a un titre vide.</string>
<string name="collections_import_error_source_blank_fields">La source '%1$d' dans le dossier '%2$s' a des champs vides.</string>
<string name="collections_import_error_trakt_list_id">La source '%1$d' dans le dossier '%2$s' n'a pas d'ID de liste Trakt.</string>
<string name="collections_import_error_invalid_json">JSON invalide : %1$s</string>
<string name="collections_folder_addon_not_found">Addon introuvable : %1$s</string>
<string name="date_month_january">Janvier</string>

View file

@ -13,7 +13,7 @@
<string name="action_previous">Previous</string>
<string name="action_remove">Remove</string>
<string name="action_reorder">Reorder</string>
<string name="action_reset">Reset</string>
<string name="action_reset">Reset to Default</string>
<string name="action_resume">Resume</string>
<string name="action_retry">Retry</string>
<string name="action_save">Save</string>
@ -361,36 +361,36 @@
<string name="compose_settings_category_general">General</string>
<string name="compose_settings_page_account">Account</string>
<string name="compose_settings_page_addons">Addons</string>
<string name="compose_settings_page_appearance">Appearance</string>
<string name="compose_settings_page_appearance">Layout</string>
<string name="compose_settings_page_content_discovery">Content &amp; Discovery</string>
<string name="compose_settings_page_continue_watching">Continue Watching</string>
<string name="compose_settings_page_homescreen">Homescreen</string>
<string name="compose_settings_page_homescreen">Home Layout</string>
<string name="compose_settings_page_integrations">Integrations</string>
<string name="compose_settings_page_mdblist_ratings">MDBList Ratings</string>
<string name="compose_settings_page_meta_screen">Meta Screen</string>
<string name="compose_settings_page_meta_screen">Detail Page</string>
<string name="compose_settings_page_notifications">Notifications</string>
<string name="compose_settings_page_playback">Playback</string>
<string name="compose_settings_page_plugins">Plugins</string>
<string name="compose_settings_page_poster_customization">Poster Customization</string>
<string name="compose_settings_page_poster_customization">Poster Card Style</string>
<string name="compose_settings_page_root">Settings</string>
<string name="compose_settings_page_supporters_contributors">Supporters &amp; Contributors</string>
<string name="compose_settings_page_tmdb_enrichment">TMDB Enrichment</string>
<string name="compose_settings_page_trakt">Trakt</string>
<string name="compose_settings_root_about_section">ABOUT</string>
<string name="compose_settings_root_account_description">Manage your account, sign out, or delete.</string>
<string name="compose_settings_root_account_description">Account and sync status</string>
<string name="compose_settings_root_account_section">ACCOUNT</string>
<string name="compose_settings_root_appearance_description">Tune home presentation and visual preferences.</string>
<string name="compose_settings_root_check_updates_description">Check for new versions of the app.</string>
<string name="compose_settings_root_appearance_description">Home structure and poster styles</string>
<string name="compose_settings_root_check_updates_description">Download latest release</string>
<string name="compose_settings_root_check_updates_title">Check for updates</string>
<string name="compose_settings_root_content_discovery_description">Manage addons and discovery sources.</string>
<string name="compose_settings_root_downloads_description">Manage your downloaded movies and episodes.</string>
<string name="compose_settings_root_downloads_title">Downloads</string>
<string name="compose_settings_root_general_section">GENERAL</string>
<string name="compose_settings_root_integrations_description">Connect TMDB and MDBList services.</string>
<string name="compose_settings_root_integrations_description">Manage available integrations</string>
<string name="compose_settings_root_notifications_description">Manage episode release alerts and send a test notification.</string>
<string name="compose_settings_root_switch_profile_description">Change to a different profile.</string>
<string name="compose_settings_root_switch_profile_title">Switch Profile</string>
<string name="compose_settings_root_trakt_description">Connect Trakt, sync watchlist lists, and save titles directly to Trakt.</string>
<string name="compose_settings_root_trakt_description">Open Trakt connection screen</string>
<string name="compose_trakt_list_picker_loading">Loading your Trakt lists…</string>
<string name="compose_trakt_list_picker_subtitle">Choose where to save this title on Trakt</string>
<string name="action_donate">Donate</string>
@ -443,13 +443,13 @@
<string name="settings_account_sign_out_confirm_title">Sign Out?</string>
<string name="settings_account_status">Status</string>
<string name="settings_account_status_anonymous">Anonymous</string>
<string name="settings_account_status_signed_in">Signed In</string>
<string name="settings_account_status_signed_in">Signed in</string>
<string name="settings_appearance_amoled_black">AMOLED Black</string>
<string name="settings_appearance_amoled_description">Use pure black backgrounds for OLED screens.</string>
<string name="settings_appearance_app_language">App Language</string>
<string name="settings_appearance_app_language_sheet_title">Choose Language</string>
<string name="settings_appearance_continue_watching_description">Show, hide, and style the Continue Watching shelf.</string>
<string name="settings_appearance_poster_customization_description">Adjust shared poster card width and corner radius presets.</string>
<string name="settings_appearance_continue_watching_description">Settings for the Continue Watching section.</string>
<string name="settings_appearance_poster_customization_description">Tune card width and corner radius.</string>
<string name="settings_appearance_section_display">DISPLAY</string>
<string name="settings_appearance_section_home">HOME</string>
<string name="settings_appearance_section_theme">THEME</string>
@ -470,22 +470,23 @@
<string name="settings_homescreen_section_catalogs">CATALOGS</string>
<string name="settings_homescreen_section_catalogs_collections">CATALOGS &amp; COLLECTIONS</string>
<string name="settings_homescreen_section_collections">COLLECTIONS</string>
<string name="settings_homescreen_section_hero">HERO</string>
<string name="settings_homescreen_section_hero_sources">HERO SOURCES</string>
<string name="settings_homescreen_section_hero">Home Layout</string>
<string name="settings_homescreen_section_hero_sources">Hero Catalogs</string>
<string name="settings_homescreen_selected_count">%1$d of %2$d selected</string>
<string name="settings_homescreen_show_hero">Show Hero</string>
<string name="settings_homescreen_show_hero_description">Display a featured hero carousel at the top of Home. Choose up to 2 source catalogs below.</string>
<string name="settings_homescreen_show_hero">Show Hero Section</string>
<string name="settings_homescreen_show_hero_description">Display hero carousel at top of home.</string>
<string name="settings_homescreen_summary">%1$d of %2$d catalogs visible • %3$d hero sources selected</string>
<string name="settings_homescreen_summary_hint">Open a catalog only when you need to rename or reorder it.</string>
<string name="settings_homescreen_visible">Visible</string>
<string name="settings_hide_secret">Hide value</string>
<string name="settings_playback_subtitle">Player, subtitles, and auto-play</string>
<string name="settings_poster_card_radius">Card Radius</string>
<string name="settings_poster_card_style">POSTER CARD STYLE</string>
<string name="settings_poster_card_width">Card Width</string>
<string name="settings_poster_card_radius">Corner Radius</string>
<string name="settings_poster_card_style">Poster Card Style</string>
<string name="settings_poster_card_width">Width</string>
<string name="settings_poster_custom">Custom</string>
<string name="settings_poster_description">Customize card width and corner radius for shared poster cards across the app.</string>
<string name="settings_poster_description">Tune card width and corner radius.</string>
<string name="settings_poster_hide_labels">Hide labels</string>
<string name="settings_poster_landscape_mode">Landscape mode for shelf posters</string>
<string name="settings_poster_landscape_mode">Landscape Posters</string>
<string name="settings_poster_live_preview">Live Preview</string>
<string name="settings_poster_option_with_value">%1$s (%2$s)</string>
<string name="settings_poster_preview_corner_radius">Corner radius: %1$ddp</string>
@ -502,9 +503,10 @@
<string name="settings_poster_width_dense">Dense</string>
<string name="settings_poster_width_large">Large</string>
<string name="settings_poster_width_standard">Standard</string>
<string name="settings_show_secret">Show value</string>
<string name="settings_continue_watching_resume_prompt_description">Show a popup to continue where you left off when opening the app after leaving from the player.</string>
<string name="settings_continue_watching_resume_prompt_title">Resume prompt on launch</string>
<string name="settings_continue_watching_section_card_style">CARD STYLE</string>
<string name="settings_continue_watching_section_card_style">Poster Card Style</string>
<string name="settings_continue_watching_section_on_launch">ON LAUNCH</string>
<string name="settings_continue_watching_section_up_next_behavior">UP NEXT BEHAVIOR</string>
<string name="settings_continue_watching_section_visibility">VISIBILITY</string>
@ -514,27 +516,27 @@
<string name="settings_continue_watching_style_poster_description">Artwork-first poster card</string>
<string name="settings_continue_watching_style_wide">Wide</string>
<string name="settings_continue_watching_style_wide_description">Info-dense horizontal card</string>
<string name="settings_continue_watching_up_next_description">When enabled, Up Next always continues from the furthest watched episode. When disabled, it follows from the most recently watched episode. Useful if you rewatch earlier episodes.</string>
<string name="settings_continue_watching_up_next_title">Up Next from furthest episode</string>
<string name="settings_continue_watching_up_next_description">Show next episode based on the furthest watched episode. Disable for rewatches to use the most recently watched episode instead.</string>
<string name="settings_continue_watching_up_next_title">Up Next From Furthest Episode</string>
<string name="settings_content_discovery_section_home">HOME</string>
<string name="settings_content_discovery_section_sources">SOURCES</string>
<string name="settings_content_discovery_addons_description">Install, remove, refresh, and sort your content sources.</string>
<string name="settings_content_discovery_plugins_description">Install JavaScript scraper repositories and test providers internally.</string>
<string name="settings_content_discovery_homescreen_description">Control which catalogs appear on Home and in what order.</string>
<string name="settings_content_discovery_meta_screen_description">Disable detail sections and reorder everything below Hero.</string>
<string name="settings_content_discovery_homescreen_description">Adjust home layout, content visibility, and poster behavior</string>
<string name="settings_content_discovery_meta_screen_description">Settings for the detail and episode screens.</string>
<string name="settings_content_discovery_collections_description">Create custom catalog groupings with folders shown on Home.</string>
<string name="settings_integrations_section_title">INTEGRATIONS</string>
<string name="settings_integrations_tmdb_description">Enhance detail pages with TMDB artwork, credits, episode metadata, and more.</string>
<string name="settings_integrations_mdblist_description">Add IMDb, Rotten Tomatoes, Metacritic, and other external ratings to details pages.</string>
<string name="settings_integrations_section_title">Integrations</string>
<string name="settings_integrations_tmdb_description">Metadata enrichment controls</string>
<string name="settings_integrations_mdblist_description">External ratings providers</string>
<string name="settings_mdb_add_api_key_first">Add your MDBList API key below before turning ratings on.</string>
<string name="settings_mdb_api_key_description">Get a key from https://mdblist.com/preferences and paste it here.</string>
<string name="settings_mdb_api_key_label">API key</string>
<string name="settings_mdb_api_key_title">MDBList API key</string>
<string name="settings_mdb_enable_ratings">Enable MDBList ratings</string>
<string name="settings_mdb_enable_ratings_description">Show external ratings from MDBList on metadata pages when an IMDb ID is available.</string>
<string name="settings_mdb_section_api_key">API KEY</string>
<string name="settings_mdb_section_rating_providers">RATING PROVIDERS</string>
<string name="settings_mdb_section_title">MDBLIST</string>
<string name="settings_mdb_api_key_description">Required to fetch ratings from MDBList</string>
<string name="settings_mdb_api_key_label">API Key</string>
<string name="settings_mdb_api_key_title">API Key</string>
<string name="settings_mdb_enable_ratings">Enable MDBList Ratings</string>
<string name="settings_mdb_enable_ratings_description">Fetch ratings from external providers in metadata detail screen</string>
<string name="settings_mdb_section_api_key">API Key</string>
<string name="settings_mdb_section_rating_providers">External ratings providers</string>
<string name="settings_mdb_section_title">MDBList Ratings</string>
<string name="settings_meta_actions">Actions</string>
<string name="settings_meta_actions_description">Play and save controls.</string>
<string name="settings_meta_cast">Cast</string>
@ -544,7 +546,7 @@
<string name="settings_meta_collection">Collection</string>
<string name="settings_meta_collection_description">Related collection or franchise rail.</string>
<string name="settings_meta_comments">Comments</string>
<string name="settings_meta_comments_description">Trakt comments section.</string>
<string name="settings_meta_comments_description">Reviews from Trakt</string>
<string name="settings_meta_details">Details</string>
<string name="settings_meta_details_description">Runtime, status, release, language, and related info.</string>
<string name="settings_meta_episode_cards">Episode Cards</string>
@ -556,8 +558,8 @@
<string name="settings_meta_episodes">Episodes</string>
<string name="settings_meta_episodes_description">Seasons and episode list for series.</string>
<string name="settings_meta_group_label">Group %1$d</string>
<string name="settings_meta_more_like_this">More Like This</string>
<string name="settings_meta_more_like_this_description">Recommendation rail.</string>
<string name="settings_meta_more_like_this">More like this</string>
<string name="settings_meta_more_like_this_description">TMDB recommendation backdrops on detail page</string>
<string name="settings_meta_none">None</string>
<string name="settings_meta_overview">Overview</string>
<string name="settings_meta_overview_description">Synopsis, ratings, genres, and core credits.</string>
@ -614,8 +616,8 @@
<string name="community_month_nov">Nov</string>
<string name="community_month_dec">Dec</string>
<string name="community_date_format">%1$s %2$s, %3$s</string>
<string name="settings_playback_all_addons">All Addons</string>
<string name="settings_playback_all_plugins">All Plugins</string>
<string name="settings_playback_all_addons">All installed addons</string>
<string name="settings_playback_all_plugins">All enabled plugins</string>
<string name="settings_playback_allowed_addons">Allowed Addons</string>
<string name="settings_playback_allowed_plugins">Allowed Plugins</string>
<string name="settings_playback_anime_skip">Anime Skip</string>
@ -626,11 +628,11 @@
<string name="settings_playback_introdb_api_key">IntroDB API Key</string>
<string name="settings_playback_introdb_api_key_description">Enter your IntroDB API key to submit timestamps. Required for submission.</string>
<string name="settings_playback_anime_skip_description">Also search AnimeSkip for skip timestamps (requires client ID).</string>
<string name="settings_playback_auto_play_next_episode">Auto-Play Next Episode</string>
<string name="settings_playback_auto_play_next_episode_description">Automatically find and play the next episode when the threshold is reached.</string>
<string name="settings_playback_decoder_device_only">Device Only</string>
<string name="settings_playback_decoder_prefer_app">Prefer App (FFmpeg)</string>
<string name="settings_playback_decoder_prefer_device">Prefer Device</string>
<string name="settings_playback_auto_play_next_episode">Auto-play Next Episode</string>
<string name="settings_playback_auto_play_next_episode_description">Start next episode automatically when prompt appears.</string>
<string name="settings_playback_decoder_device_only">Device decoders only</string>
<string name="settings_playback_decoder_prefer_app">Prefer app decoders (FFmpeg)</string>
<string name="settings_playback_decoder_prefer_device">Prefer device decoders</string>
<string name="settings_playback_decoder_priority">Decoder Priority</string>
<string name="settings_playback_dialog_close">Tap outside to close</string>
<string name="settings_playback_dialog_save_close">Tap outside to save &amp; close</string>
@ -638,32 +640,32 @@
<string name="settings_playback_duration_days">%1$d days</string>
<string name="settings_playback_duration_hour_one">%1$d hour</string>
<string name="settings_playback_duration_hours">%1$d hours</string>
<string name="settings_playback_enable_libass">Enable libass</string>
<string name="settings_playback_enable_libass_description">Use libass for ASS/SSA subtitle rendering instead of the default renderer.</string>
<string name="settings_playback_enable_libass">Use libass for ASS/SSA subtitles</string>
<string name="settings_playback_enable_libass_description">Experimental: advanced ASS/SSA rendering (styles, positioning, animations)</string>
<string name="settings_playback_hold_speed">Hold Speed</string>
<string name="settings_playback_hold_to_speed">Hold To Speed</string>
<string name="settings_playback_hold_to_speed_description">Long-press anywhere on the player surface to temporarily boost playback speed.</string>
<string name="settings_playback_invalid_regex_pattern">Invalid regex pattern</string>
<string name="settings_playback_last_link_cache_duration">Last Link Cache Duration</string>
<string name="settings_playback_map_dv7_to_hevc">Map DV7 to HEVC</string>
<string name="settings_playback_map_dv7_to_hevc_description">Dolby Vision Profile 7 to HEVC fallback for unsupported devices.</string>
<string name="settings_playback_minutes_before_end">Minutes Before End</string>
<string name="settings_playback_minutes_before_end_description">Show next episode card this many minutes before the end.</string>
<string name="settings_playback_map_dv7_to_hevc">DV7 - HEVC Fallback</string>
<string name="settings_playback_map_dv7_to_hevc_description">Map Dolby Vision Profile 7 to standard HEVC for devices without DV hardware support</string>
<string name="settings_playback_minutes_before_end">Threshold Minutes</string>
<string name="settings_playback_minutes_before_end_description">Fallback when no outro timestamp exists.</string>
<string name="settings_playback_minutes_value">%1$d min</string>
<string name="settings_playback_no_items_available">No items available</string>
<string name="settings_playback_not_set">Not set</string>
<string name="settings_playback_option_default">Default</string>
<string name="settings_playback_option_device_language">Device Language</string>
<string name="settings_playback_option_default">Default (media file)</string>
<string name="settings_playback_option_device_language">Device language</string>
<string name="settings_playback_option_forced">Forced</string>
<string name="settings_playback_option_none">None</string>
<string name="settings_playback_prefer_binge_group">Prefer Binge Group</string>
<string name="settings_playback_prefer_binge_group_description">When auto-playing, prefer a stream from the same binge group as the current one.</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_preferred_audio_language">Preferred Audio Language</string>
<string name="settings_playback_preferred_subtitle_language">Preferred Subtitle Language</string>
<string name="settings_playback_preferred_subtitle_language">Preferred Language</string>
<string name="settings_playback_presets">Presets</string>
<string name="settings_playback_regex_matches_against">Matches against stream name, label, description, addon, and URL.</string>
<string name="settings_playback_regex_matches_against">Matches against stream name/title/description/addon/url. Example: 4K|2160p|Remux</string>
<string name="settings_playback_regex_pattern">Regex Pattern</string>
<string name="settings_playback_regex_placeholder">4K|2160p|Remux</string>
<string name="settings_playback_regex_placeholder">No pattern set. Example: 4K|2160p|Remux</string>
<string name="settings_playback_regex_preset_any_1080p">Any 1080p+</string>
<string name="settings_playback_regex_preset_avc_x264">AVC / x264</string>
<string name="settings_playback_regex_preset_bluray_quality">BluRay Quality</string>
@ -677,16 +679,16 @@
<string name="settings_playback_regex_preset_quality_4k_remux">4K / Remux</string>
<string name="settings_playback_regex_preset_quality_720p_smaller">720p / Smaller</string>
<string name="settings_playback_regex_preset_web_sources">WEB Sources</string>
<string name="settings_playback_render_type">Render Type</string>
<string name="settings_playback_render_type_cues">Standard (Cues)</string>
<string name="settings_playback_render_type">Libass Render Mode</string>
<string name="settings_playback_render_type_cues">Standard Cues</string>
<string name="settings_playback_render_type_effects_canvas">Effects Canvas</string>
<string name="settings_playback_render_type_effects_opengl">Effects 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_render_type_overlay_opengl">Overlay OpenGL (Recommended)</string>
<string name="settings_playback_reuse_last_link">Reuse Last Link</string>
<string name="settings_playback_reuse_last_link_description">Auto-play your last working stream for this same movie/episode when cache is still valid.</string>
<string name="settings_playback_reuse_last_link_description">Auto-play your last working stream for this same movie/episode when cache is still valid</string>
<string name="settings_playback_secondary_audio_language">Secondary Audio Language</string>
<string name="settings_playback_secondary_subtitle_language">Secondary Subtitle Language</string>
<string name="settings_playback_secondary_subtitle_language">Secondary Preferred Language</string>
<string name="settings_playback_section_decoder">DECODER</string>
<string name="settings_playback_section_next_episode">NEXT EPISODE</string>
<string name="settings_playback_section_player">PLAYER</string>
@ -696,79 +698,79 @@
<string name="settings_playback_section_subtitle_audio">SUBTITLE AND AUDIO</string>
<string name="settings_playback_section_subtitle_rendering">SUBTITLE RENDERING</string>
<string name="settings_playback_selected_count">%1$d selected</string>
<string name="settings_playback_show_loading_overlay">Show Loading Overlay</string>
<string name="settings_playback_show_loading_overlay_description">Show the opening loading overlay while a stream starts playing.</string>
<string name="settings_playback_skip_intro_outro_recap">Skip Intro/Outro/Recap</string>
<string name="settings_playback_skip_intro_outro_recap_description">Show skip button during detected intro, outro, and recap segments.</string>
<string name="settings_playback_source_scope">Source Scope</string>
<string name="settings_playback_source_scope_all_addons">All Addons</string>
<string name="settings_playback_source_scope_all_addons_description">Consider streams from all installed addons.</string>
<string name="settings_playback_source_scope_all_sources">All Sources</string>
<string name="settings_playback_source_scope_all_sources_description">Consider streams from both addons and plugins.</string>
<string name="settings_playback_source_scope_enabled_plugins_only">Enabled Plugins Only</string>
<string name="settings_playback_source_scope_enabled_plugins_only_description">Only consider streams from enabled plugins.</string>
<string name="settings_playback_source_scope_installed_addons_only">Installed Addons Only</string>
<string name="settings_playback_source_scope_installed_addons_only_description">Only consider streams from installed addons.</string>
<string name="settings_playback_stream_selection_mode">Stream Selection Mode</string>
<string name="settings_playback_stream_selection_mode_first_stream">First Available Stream</string>
<string name="settings_playback_stream_selection_mode_first_stream_description">Automatically play the first stream found.</string>
<string name="settings_playback_stream_selection_mode_manual">Manual</string>
<string name="settings_playback_stream_selection_mode_manual_description">Select streams manually each time.</string>
<string name="settings_playback_stream_selection_mode_regex">Regex Match</string>
<string name="settings_playback_stream_selection_mode_regex_description">Auto-select a stream matching a regex pattern.</string>
<string name="settings_playback_stream_timeout">Stream Timeout</string>
<string name="settings_playback_stream_timeout_description">How long to wait for streams before auto-selecting.</string>
<string name="settings_playback_threshold_minutes">Minutes Before End</string>
<string name="settings_playback_threshold_mode">Threshold Mode</string>
<string name="settings_playback_threshold_mode_minutes_before_end">Minutes Before End</string>
<string name="settings_playback_show_loading_overlay">Loading Overlay</string>
<string name="settings_playback_show_loading_overlay_description">Show loading screen until first video frame appears.</string>
<string name="settings_playback_skip_intro_outro_recap">Skip Intro</string>
<string name="settings_playback_skip_intro_outro_recap_description">Use introdb.app to detect intros and recaps.</string>
<string name="settings_playback_source_scope">Auto-play Source Scope</string>
<string name="settings_playback_source_scope_all_addons">All installed addons</string>
<string name="settings_playback_source_scope_all_addons_description">Auto-play only considers streams coming from your installed addons.</string>
<string name="settings_playback_source_scope_all_sources">All sources</string>
<string name="settings_playback_source_scope_all_sources_description">Auto-play can use both installed addons and enabled plugins.</string>
<string name="settings_playback_source_scope_enabled_plugins_only">Enabled plugins only</string>
<string name="settings_playback_source_scope_enabled_plugins_only_description">Auto-play only considers streams coming from enabled plugins.</string>
<string name="settings_playback_source_scope_installed_addons_only">Installed addons only</string>
<string name="settings_playback_source_scope_installed_addons_only_description">Auto-play only considers streams coming from your installed addons.</string>
<string name="settings_playback_stream_selection_mode">Auto Stream Selection</string>
<string name="settings_playback_stream_selection_mode_first_stream">Auto-play first source</string>
<string name="settings_playback_stream_selection_mode_first_stream_description">Play the first available source automatically.</string>
<string name="settings_playback_stream_selection_mode_manual">Manual (choose stream)</string>
<string name="settings_playback_stream_selection_mode_manual_description">Always show source list and let me choose.</string>
<string name="settings_playback_stream_selection_mode_regex">Auto-play regex match</string>
<string name="settings_playback_stream_selection_mode_regex_description">Play first source whose text matches your regex pattern.</string>
<string name="settings_playback_stream_timeout">Stream Selection Timeout</string>
<string name="settings_playback_stream_timeout_description">Wait time for addons before selecting.</string>
<string name="settings_playback_threshold_minutes">Threshold Minutes</string>
<string name="settings_playback_threshold_mode">Next Episode Threshold Mode</string>
<string name="settings_playback_threshold_mode_minutes_before_end">Minutes before end</string>
<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_description">Fallback when no outro timestamp exists.</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>
<string name="settings_playback_tunneled_playback">Tunneled Playback</string>
<string name="settings_playback_tunneled_playback_description">Enable tunneled playback for lower latency audio/video sync.</string>
<string name="settings_playback_tunneled_playback_description">Hardware-level audio/video sync. May improve playback on some Android TV devices</string>
<string name="settings_tmdb_add_api_key_first">Add your own TMDB API key below before turning enrichment on.</string>
<string name="settings_tmdb_api_key_label">TMDB API key</string>
<string name="settings_tmdb_enable_enrichment">Enable TMDB enrichment</string>
<string name="settings_tmdb_enable_enrichment_description">Use your TMDB API key to enrich addon metadata on the details screen when a TMDB or IMDb ID is available.</string>
<string name="settings_tmdb_api_key_label">API Key</string>
<string name="settings_tmdb_enable_enrichment">Enable TMDB Enrichment</string>
<string name="settings_tmdb_enable_enrichment_description">Use TMDB as a metadata source to enhance addon data</string>
<string name="settings_tmdb_enter_api_key">Enter your TMDB v3 API key.</string>
<string name="settings_tmdb_language_code_label">Language code</string>
<string name="settings_tmdb_module_artwork">Artwork</string>
<string name="settings_tmdb_module_artwork_description">Replace backdrop, poster, and logo with TMDB artwork.</string>
<string name="settings_tmdb_module_basic_info">Basic info</string>
<string name="settings_tmdb_module_basic_info_description">Use TMDB title, synopsis, genres, and rating.</string>
<string name="settings_tmdb_module_artwork_description">Logo and backdrop images from TMDB</string>
<string name="settings_tmdb_module_basic_info">Basic Info</string>
<string name="settings_tmdb_module_basic_info_description">Description, genres, and rating from TMDB</string>
<string name="settings_tmdb_module_collections">Collections</string>
<string name="settings_tmdb_module_collections_description">Show franchise and collection rails for movies when TMDB provides them.</string>
<string name="settings_tmdb_module_collections_description">TMDB movie collections in release order</string>
<string name="settings_tmdb_module_credits">Credits</string>
<string name="settings_tmdb_module_credits_description">Use TMDB creators, directors, writers, and cast photos.</string>
<string name="settings_tmdb_module_credits_description">Cast with photos, director, and writer from TMDB</string>
<string name="settings_tmdb_module_details">Details</string>
<string name="settings_tmdb_module_details_description">Use TMDB release info, runtime, age rating, status, country, and language.</string>
<string name="settings_tmdb_module_details_description">Runtime, status, country, and language from TMDB</string>
<string name="settings_tmdb_module_episodes">Episodes</string>
<string name="settings_tmdb_module_episodes_description">Use TMDB episode titles, thumbnails, descriptions, and runtimes for series.</string>
<string name="settings_tmdb_module_more_like_this">More like this</string>
<string name="settings_tmdb_module_more_like_this_description">Show TMDB recommendations at the bottom of detail pages.</string>
<string name="settings_tmdb_module_episodes_description">Episode titles, overviews, thumbnails, and runtime from TMDB</string>
<string name="settings_tmdb_module_more_like_this">More Like This</string>
<string name="settings_tmdb_module_more_like_this_description">TMDB recommendation backdrops on detail page</string>
<string name="settings_tmdb_module_networks">Networks</string>
<string name="settings_tmdb_module_networks_description">Use TMDB network metadata for TV titles.</string>
<string name="settings_tmdb_module_production_companies">Production companies</string>
<string name="settings_tmdb_module_production_companies_description">Use TMDB production company metadata on the details screen.</string>
<string name="settings_tmdb_module_networks_description">Networks with logos from TMDB</string>
<string name="settings_tmdb_module_production_companies">Productions</string>
<string name="settings_tmdb_module_production_companies_description">Production companies from TMDB</string>
<string name="settings_tmdb_module_season_posters">Season posters</string>
<string name="settings_tmdb_module_season_posters_description">Use TMDB season posters in the metadata screen season selector for series.</string>
<string name="settings_tmdb_module_trailers">Trailers</string>
<string name="settings_tmdb_module_trailers_description">Fetch and show TMDB trailer videos section on detail pages.</string>
<string name="settings_tmdb_module_trailers_description">Trailer candidates from TMDB videos for the detail trailer section</string>
<string name="settings_tmdb_personal_api_key">Personal API key</string>
<string name="settings_tmdb_preferred_language">Preferred language</string>
<string name="settings_tmdb_preferred_language_description">Set the TMDB language code used for localized metadata, for example `en`, `en-US`, or `pt-BR`.</string>
<string name="settings_tmdb_preferred_language">Language</string>
<string name="settings_tmdb_preferred_language_description">TMDB metadata language for title, logo, and enabled fields</string>
<string name="settings_tmdb_section_credentials">CREDENTIALS</string>
<string name="settings_tmdb_section_localization">LOCALIZATION</string>
<string name="settings_tmdb_section_modules">MODULES</string>
<string name="settings_tmdb_section_title">TMDB</string>
<string name="settings_tmdb_section_title">TMDB Enrichment</string>
<string name="settings_trakt_approval_redirect">After approval, you will be redirected back automatically.</string>
<string name="settings_trakt_authentication">AUTHENTICATION</string>
<string name="settings_trakt_comments">Comments</string>
<string name="settings_trakt_comments_description">Show Trakt comments on movie and show details</string>
<string name="settings_trakt_comments_description">Show Trakt reviews on metadata pages</string>
<string name="settings_trakt_connect">Connect Trakt</string>
<string name="settings_trakt_connected_as">Connected as %1$s</string>
<string name="settings_trakt_default_user">Trakt user</string>
@ -776,7 +778,7 @@
<string name="settings_trakt_failed_open_browser">Failed to open browser</string>
<string name="settings_trakt_features">FEATURES</string>
<string name="settings_trakt_finish_sign_in">Finish Trakt sign in in your browser</string>
<string name="settings_trakt_intro_description">Track what you watch, save to watchlist or custom lists, and keep your library synced with Trakt.</string>
<string name="settings_trakt_intro_description">Sync your watchlist, watch progress, continue watching, scrobbles, and personal lists with Trakt.</string>
<string name="settings_trakt_missing_credentials">Missing Trakt credentials in local.properties (TRAKT_CLIENT_ID / TRAKT_CLIENT_SECRET).</string>
<string name="settings_trakt_open_login">Open Trakt Login</string>
<string name="settings_trakt_save_actions_description">Your Save actions can now target Trakt watchlist and personal lists.</string>
@ -1024,6 +1026,7 @@
<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="streams_torrent_not_supported">Torrent streams are not supported</string>
<string name="trailer_close">Close trailer</string>
<string name="trailer_unable_to_play">Unable to play trailer</string>
<string name="trakt_lists_load_failed">Failed to load Trakt lists</string>

View file

@ -62,7 +62,7 @@ data class CollectionSource(
addonId = sourceAddonId,
type = sourceType,
catalogId = sourceCatalogId,
genre = genre,
genre = genre.normalizedOptionalGenre(),
)
}
}
@ -193,7 +193,7 @@ data class CollectionFolder(
addonId = source.addonId,
type = source.type,
catalogId = source.catalogId,
genre = source.genre,
genre = source.genre.normalizedOptionalGenre(),
)
}
}
@ -217,6 +217,11 @@ data class Collection(
get() = FolderViewMode.fromString(viewMode)
}
private fun String?.normalizedOptionalGenre(): String? =
this
?.trim()
?.takeIf { it.isNotEmpty() && !it.equals("none", ignoreCase = true) }
data class AvailableCatalog(
val addonId: String,
val addonName: String,

View file

@ -358,7 +358,7 @@ private fun HeroContentBlock(
modifier = Modifier
.fillMaxWidth(layout.logoWidthFraction)
.aspectRatio(2.6f)
.clickable(enabled = !layout.isTablet && onItemClick != null) {
.clickable(enabled = onItemClick != null) {
onItemClick?.invoke(item)
},
alignment = if (layout.isTablet) Alignment.CenterStart else Alignment.Center,
@ -369,7 +369,7 @@ private fun HeroContentBlock(
text = item.name,
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = !layout.isTablet && onItemClick != null) {
.clickable(enabled = onItemClick != null) {
onItemClick?.invoke(item)
},
style = if (layout.isTablet) {

View file

@ -19,7 +19,7 @@ data class PlayerAudioLevel(
expect fun LockPlayerToLandscape()
@Composable
expect fun EnterImmersivePlayerMode()
expect fun EnterImmersivePlayerMode(keepScreenAwake: Boolean)
@Composable
expect fun ManagePlayerPictureInPicture(

View file

@ -139,7 +139,6 @@ fun PlayerScreen(
initialProgressFraction: Float? = null,
) {
LockPlayerToLandscape()
EnterImmersivePlayerMode()
val playerSettingsUiState by remember {
PlayerSettingsRepository.ensureLoaded()
PlayerSettingsRepository.uiState
@ -195,6 +194,9 @@ fun PlayerScreen(
var playerController by remember { mutableStateOf<PlayerEngineController?>(null) }
var playerControllerSourceUrl by remember { mutableStateOf<String?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val keepScreenAwake = errorMessage == null &&
(playbackSnapshot.isPlaying || (shouldPlay && playbackSnapshot.isLoading))
EnterImmersivePlayerMode(keepScreenAwake = keepScreenAwake)
var scrubbingPositionMs by remember { mutableStateOf<Long?>(null) }
var pausedOverlayVisible by remember { mutableStateOf(false) }
var gestureFeedback by remember { mutableStateOf<GestureFeedbackState?>(null) }

View file

@ -3,6 +3,7 @@ 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_german
import nuvio.composeapp.generated.resources.lang_spanish
import nuvio.composeapp.generated.resources.lang_portuguese_portugal
import nuvio.composeapp.generated.resources.lang_turkish
@ -17,6 +18,7 @@ enum class AppLanguage(
) {
ENGLISH("en", Res.string.lang_english),
FRENCH("fr", Res.string.lang_french),
GERMAN("de", Res.string.lang_german),
SPANISH("es", Res.string.lang_spanish),
PORTUGUESE("pt", Res.string.lang_portuguese_portugal),
TURKISH("tr", Res.string.lang_turkish),

View file

@ -6,11 +6,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -19,7 +16,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.nuvio.app.features.mdblist.MdbListMetadataService
import com.nuvio.app.features.mdblist.MdbListSettings
@ -170,22 +166,13 @@ private fun MdbListApiKeyRow(
)
}
OutlinedTextField(
SettingsSecretTextField(
value = draft,
onValueChange = {
draft = it
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
label = { Text(stringResource(Res.string.settings_mdb_api_key_label)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.42f),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledContainerColor = MaterialTheme.colorScheme.surface,
),
label = stringResource(Res.string.settings_mdb_api_key_label),
)
Row(modifier = Modifier.fillMaxWidth()) {

View file

@ -1960,27 +1960,16 @@ private fun IntroDbApiKeyDialog(
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = if (errorMessage != null) 1f else 0.3f)),
) {
BasicTextField(
SettingsSecretTextField(
value = value,
onValueChange = {
value = it
errorMessage = null
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 12.dp),
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface,
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
singleLine = true,
label = stringResource(Res.string.settings_playback_introdb_api_key),
modifier = Modifier.fillMaxWidth(),
isError = errorMessage != null,
)
}
if (errorMessage != null) {
Text(
text = errorMessage!!,
@ -2162,4 +2151,3 @@ private fun libassRenderTypeRes(renderType: String): StringResource = when (rend
@Composable
private fun libassRenderTypeLabel(renderType: String): String = stringResource(libassRenderTypeRes(renderType))

View file

@ -16,9 +16,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -30,6 +31,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@ -296,6 +298,8 @@ private fun MobileSettingsScreen(
onCheckForUpdatesClick: (() -> Unit)? = null,
onCollectionsClick: () -> Unit = {},
) {
val saveableStateHolder = rememberSaveableStateHolder()
saveableStateHolder.SaveableStateProvider(page.name) {
NuvioScreen {
stickyHeader {
val previousPage = page.previousPage()
@ -411,6 +415,7 @@ private fun MobileSettingsScreen(
}
}
}
}
@Composable
private fun TabletSettingsScreen(
@ -468,6 +473,8 @@ private fun TabletSettingsScreen(
onPageChange(page)
}
val saveableStateHolder = rememberSaveableStateHolder()
Row(modifier = Modifier.fillMaxSize()) {
Surface(
modifier = Modifier
@ -510,7 +517,10 @@ private fun TabletSettingsScreen(
}
}
saveableStateHolder.SaveableStateProvider(page.name) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = 40.dp,
@ -642,3 +652,4 @@ private fun TabletSettingsScreen(
}
}
}
}

View file

@ -0,0 +1,69 @@
package com.nuvio.app.features.settings
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.settings_hide_secret
import nuvio.composeapp.generated.resources.settings_show_secret
import org.jetbrains.compose.resources.stringResource
@Composable
internal fun SettingsSecretTextField(
value: String,
onValueChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
isError: Boolean = false,
) {
var visible by rememberSaveable { mutableStateOf(false) }
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
isError = isError,
singleLine = true,
label = { Text(label) },
visualTransformation = if (visible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
IconButton(onClick = { visible = !visible }) {
Icon(
imageVector = if (visible) Icons.Rounded.VisibilityOff else Icons.Rounded.Visibility,
contentDescription = stringResource(
if (visible) Res.string.settings_hide_secret else Res.string.settings_show_secret,
),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.42f),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledContainerColor = MaterialTheme.colorScheme.surface,
),
)
}

View file

@ -265,21 +265,13 @@ private fun TmdbApiKeyRow(
val normalizedDraft = draft.trim()
OutlinedTextField(
SettingsSecretTextField(
value = draft,
onValueChange = {
draft = it
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
label = { Text(stringResource(Res.string.settings_tmdb_api_key_label)) },
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.42f),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledContainerColor = MaterialTheme.colorScheme.surface,
),
label = stringResource(Res.string.settings_tmdb_api_key_label),
)
Row(modifier = Modifier.fillMaxWidth()) {

View file

@ -25,10 +25,18 @@ data class StreamItem(
val directPlaybackUrl: String?
get() = url ?: externalUrl
val isTorrentStream: Boolean
get() = !infoHash.isNullOrBlank() ||
url.isMagnetLink() ||
externalUrl.isMagnetLink()
val hasPlayableSource: Boolean
get() = url != null || infoHash != null || externalUrl != null
}
private fun String?.isMagnetLink(): Boolean =
this?.trimStart()?.startsWith("magnet:", ignoreCase = true) == true
data class StreamBehaviorHints(
val bingeGroup: String? = null,
val notWebReady: Boolean = false,

View file

@ -129,6 +129,7 @@ fun StreamsScreen(
val clipboardManager = LocalClipboardManager.current
val streamLinkCopiedText = stringResource(Res.string.streams_link_copied)
val noDirectStreamLinkText = stringResource(Res.string.streams_no_direct_link)
val torrentUnsupportedText = stringResource(Res.string.streams_torrent_not_supported)
var streamActionsTarget by remember(videoId) { mutableStateOf<StreamItem?>(null) }
var preferredFilterApplied by remember(videoId) { mutableStateOf(false) }
val storedProgress = if (startFromBeginning) {
@ -205,7 +206,13 @@ fun StreamsScreen(
uiState = uiState,
resumePositionMs = effectiveResumePositionMs,
resumeProgressFraction = effectiveResumeProgressFraction,
onStreamSelected = onStreamSelected,
onStreamSelected = { stream, positionMs, progressFraction ->
if (stream.isTorrentStream) {
NuvioToastController.show(torrentUnsupportedText)
} else {
onStreamSelected(stream, positionMs, progressFraction)
}
},
onStreamLongPress = { stream -> streamActionsTarget = stream },
)
} else {
@ -220,7 +227,13 @@ fun StreamsScreen(
uiState = uiState,
resumePositionMs = effectiveResumePositionMs,
resumeProgressFraction = effectiveResumeProgressFraction,
onStreamSelected = onStreamSelected,
onStreamSelected = { stream, positionMs, progressFraction ->
if (stream.isTorrentStream) {
NuvioToastController.show(torrentUnsupportedText)
} else {
onStreamSelected(stream, positionMs, progressFraction)
}
},
onStreamLongPress = { stream -> streamActionsTarget = stream },
)
}
@ -830,7 +843,7 @@ private fun LazyListScope.streamSection(
StreamCard(
stream = stream,
onClick = {
if (stream.directPlaybackUrl != null) {
if (stream.directPlaybackUrl != null || stream.isTorrentStream) {
onStreamSelected(stream, resumePositionMs, resumeProgressFraction)
}
},
@ -936,7 +949,7 @@ private fun StreamCard(
onLongClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
val isEnabled = stream.directPlaybackUrl != null
val isEnabled = stream.directPlaybackUrl != null || stream.isTorrentStream
val cardShape = RoundedCornerShape(12.dp)
Row(
modifier = modifier

View file

@ -1,8 +1,7 @@
package com.nuvio.app.features.watching.domain
private const val InProgressStartThresholdFraction = 0.02f
private const val CompletionThresholdFraction = 0.85
private const val InProgressStartThresholdMinMs = 30_000L
private const val CompletionThresholdFraction = 0.90
private const val ProgressStoreThresholdMs = 1_000L
private const val UpcomingNextSeasonWindowDays = 7
fun watchedKey(
@ -14,17 +13,7 @@ fun watchedKey(
fun shouldStoreProgress(
positionMs: Long,
durationMs: Long,
): Boolean {
val thresholdMs = if (durationMs > 0L) {
maxOf(
InProgressStartThresholdMinMs,
(durationMs * InProgressStartThresholdFraction).toLong(),
)
} else {
1L
}
return positionMs >= thresholdMs
}
): Boolean = positionMs >= ProgressStoreThresholdMs
fun isProgressComplete(
positionMs: Long,

View file

@ -26,17 +26,17 @@ class WatchProgressRulesTest {
}
@Test
fun `save threshold uses max of thirty seconds and two percent`() {
assertFalse(shouldStoreWatchProgress(positionMs = 29_999L, durationMs = 600_000L))
assertTrue(shouldStoreWatchProgress(positionMs = 30_000L, durationMs = 600_000L))
assertFalse(shouldStoreWatchProgress(positionMs = 119_999L, durationMs = 6_000_000L))
assertTrue(shouldStoreWatchProgress(positionMs = 120_000L, durationMs = 6_000_000L))
fun `save threshold starts after one second`() {
assertFalse(shouldStoreWatchProgress(positionMs = 999L, durationMs = 600_000L))
assertTrue(shouldStoreWatchProgress(positionMs = 1_000L, durationMs = 600_000L))
assertTrue(shouldStoreWatchProgress(positionMs = 1_000L, durationMs = 0L))
}
@Test
fun `completion detects watched threshold remaining time and ended state`() {
assertTrue(isWatchProgressComplete(positionMs = 920_000L, durationMs = 1_000_000L, isEnded = false))
assertTrue(isWatchProgressComplete(positionMs = 850_000L, durationMs = 1_000_000L, isEnded = false))
assertTrue(isWatchProgressComplete(positionMs = 900_000L, durationMs = 1_000_000L, isEnded = false))
assertFalse(isWatchProgressComplete(positionMs = 899_999L, durationMs = 1_000_000L, isEnded = false))
assertTrue(isWatchProgressComplete(positionMs = 1L, durationMs = 0L, isEnded = true))
assertFalse(isWatchProgressComplete(positionMs = 200_000L, durationMs = 1_000_000L, isEnded = false))
}

View file

@ -2,6 +2,7 @@ package com.nuvio.app.features.player
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.IntSize
import platform.Foundation.NSNotificationCenter
@ -32,10 +33,12 @@ actual fun LockPlayerToLandscape() {
}
@Composable
actual fun EnterImmersivePlayerMode() {
actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) {
SideEffect {
UIApplication.sharedApplication.setIdleTimerDisabled(keepScreenAwake)
}
DisposableEffect(Unit) {
// Request idle timer disabled to keep screen awake during playback
UIApplication.sharedApplication.setIdleTimerDisabled(true)
onDispose {
UIApplication.sharedApplication.setIdleTimerDisabled(false)
}

View file

@ -579,15 +579,29 @@ final class MPVPlayerViewController: UIViewController {
for i in 0..<count {
let type = getString("track-list/\(i)/type") ?? ""
let id = getInt("track-list/\(i)/id")
let title = getString("track-list/\(i)/title") ?? ""
let lang = getString("track-list/\(i)/lang") ?? ""
let title = getTrackString(i, "title")
let lang = getTrackString(i, "lang")
let codec = getTrackString(i, "codec")
let decoderDescription = getTrackString(i, "decoder-desc")
let channels = getTrackString(i, "demux-channels")
let channelCount = getInt("track-list/\(i)/demux-channel-count")
let selected = getFlag("track-list/\(i)/selected")
let displayTitle = formatTrackTitle(
type: type,
index: type == "audio" ? audioIdx : subIdx,
title: title,
lang: lang,
codec: codec,
decoderDescription: decoderDescription,
channels: channels,
channelCount: channelCount
)
if type == "audio" {
audio.append(TrackInfo(index: audioIdx, id: id, type: type, title: title, lang: lang, selected: selected))
audio.append(TrackInfo(index: audioIdx, id: id, type: type, title: displayTitle, lang: lang, selected: selected))
audioIdx += 1
} else if type == "sub" {
subs.append(TrackInfo(index: subIdx, id: id, type: type, title: title, lang: lang, selected: selected))
subs.append(TrackInfo(index: subIdx, id: id, type: type, title: displayTitle, lang: lang, selected: selected))
subIdx += 1
}
}
@ -595,6 +609,98 @@ final class MPVPlayerViewController: UIViewController {
subtitleTracks = subs
}
private func getTrackString(_ index: Int, _ field: String) -> String {
(getString("track-list/\(index)/\(field)") ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
private func formatTrackTitle(
type: String,
index: Int,
title: String,
lang: String,
codec: String,
decoderDescription: String,
channels: String,
channelCount: Int
) -> String {
let base = ifNotBlank(title)
?? localizedLanguageName(lang)
?? (type == "sub" ? "Subtitle \(index + 1)" : "Track \(index + 1)")
let codecName = codecDisplayName(codec) ?? codecDisplayName(decoderDescription)
let channelName = type == "audio" ? channelLayoutName(channels: channels, channelCount: channelCount) : nil
let details = [channelName, codecName]
.compactMap { $0 }
.filter { detail in !base.localizedCaseInsensitiveContains(detail) }
return details.isEmpty ? base : "\(base) (\(details.joined(separator: ", ")))"
}
private func ifNotBlank(_ value: String) -> String? {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private func localizedLanguageName(_ languageCode: String) -> String? {
guard let code = ifNotBlank(languageCode) else { return nil }
return Locale.current.localizedString(forLanguageCode: code) ?? code
}
private func channelLayoutName(channels: String, channelCount: Int) -> String? {
if let normalized = ifNotBlank(channels), normalized != "unknown" {
let lower = normalized.lowercased()
if lower == "mono" { return "Mono" }
if lower == "stereo" { return "Stereo" }
return normalized
}
switch channelCount {
case 1:
return "Mono"
case 2:
return "Stereo"
case 6:
return "5.1"
case 8:
return "7.1"
case let count where count > 0:
return "\(count)ch"
default:
return nil
}
}
private func codecDisplayName(_ value: String) -> String? {
guard let raw = ifNotBlank(value) else { return nil }
let codec = raw.lowercased()
if codec.contains("eac3") || codec.contains("e-ac-3") || codec.contains("e ac-3") {
return codec.contains("joc") || codec.contains("atmos") ? "E-AC-3-JOC" : "E-AC-3"
}
if codec.contains("truehd") || codec.contains("true hd") { return "TrueHD" }
if codec.contains("ac3") || codec.contains("ac-3") { return "AC-3" }
if codec.contains("dts-hd") || codec.contains("dtshd") || codec.contains("dts hd") { return "DTS-HD" }
if codec.contains("dts") || codec == "dca" { return "DTS" }
if codec.contains("aac") { return "AAC" }
if codec.contains("mp3") || codec.contains("mpeg audio") { return "MP3" }
if codec.contains("mp2") { return "MP2" }
if codec.contains("opus") { return "Opus" }
if codec.contains("vorbis") { return "Vorbis" }
if codec.contains("flac") { return "FLAC" }
if codec.contains("alac") { return "ALAC" }
if codec.contains("pcm") || codec.contains("wav") { return "WAV" }
if codec.contains("amr_wb") || codec.contains("amr-wb") { return "AMR-WB" }
if codec.contains("amr_nb") || codec.contains("amr-nb") { return "AMR-NB" }
if codec.contains("amr") { return "AMR" }
if codec.contains("iamf") { return "IAMF" }
if codec.contains("mpegh") || codec.contains("mpeg-h") { return "MPEG-H" }
if codec.contains("pgs") || codec.contains("hdmv") { return "PGS" }
if codec.contains("subrip") || codec == "srt" { return "SRT" }
if codec.contains("ass") || codec.contains("ssa") { return "SSA" }
if codec.contains("webvtt") || codec == "vtt" { return "VTT" }
if codec.contains("ttml") { return "TTML" }
if codec.contains("mov_text") || codec.contains("tx3g") { return "TX3G" }
if codec.contains("dvb") { return "DVB" }
return raw
}
private func clearPlaybackError() {
errorStateLock.lock()
recentPlaybackLogs.removeAll(keepingCapacity: true)