mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-18 07:51:46 +00:00
Merge branch 'cmp-rewrite' into trailer-fullscreen-player
This commit is contained in:
commit
7c941fefe3
23 changed files with 1991 additions and 501 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ actual fun LockPlayerToLandscape() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
actual fun EnterImmersivePlayerMode() {
|
||||
actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) {
|
||||
val activity = LocalContext.current.findActivity() ?: return
|
||||
|
||||
DisposableEffect(activity) {
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@
|
|||
<locale android:name="it"/>
|
||||
<locale android:name="el"/>
|
||||
<locale android:name="pl"/>
|
||||
<locale android:name="de"/>
|
||||
</locale-config>
|
||||
|
|
|
|||
1199
composeApp/src/commonMain/composeResources/values-de/strings.xml
Normal file
1199
composeApp/src/commonMain/composeResources/values-de/strings.xml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 Wi‑Fi 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>
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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 & 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 & 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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ data class PlayerAudioLevel(
|
|||
expect fun LockPlayerToLandscape()
|
||||
|
||||
@Composable
|
||||
expect fun EnterImmersivePlayerMode()
|
||||
expect fun EnterImmersivePlayerMode(keepScreenAwake: Boolean)
|
||||
|
||||
@Composable
|
||||
expect fun ManagePlayerPictureInPicture(
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
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,
|
||||
)
|
||||
}
|
||||
SettingsSecretTextField(
|
||||
value = value,
|
||||
onValueChange = {
|
||||
value = it
|
||||
errorMessage = null
|
||||
},
|
||||
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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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,118 +298,121 @@ private fun MobileSettingsScreen(
|
|||
onCheckForUpdatesClick: (() -> Unit)? = null,
|
||||
onCollectionsClick: () -> Unit = {},
|
||||
) {
|
||||
NuvioScreen {
|
||||
stickyHeader {
|
||||
val previousPage = page.previousPage()
|
||||
NuvioScreenHeader(
|
||||
title = stringResource(page.titleRes),
|
||||
onBack = previousPage?.let { { onPageChange(it) } },
|
||||
)
|
||||
}
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
saveableStateHolder.SaveableStateProvider(page.name) {
|
||||
NuvioScreen {
|
||||
stickyHeader {
|
||||
val previousPage = page.previousPage()
|
||||
NuvioScreenHeader(
|
||||
title = stringResource(page.titleRes),
|
||||
onBack = previousPage?.let { { onPageChange(it) } },
|
||||
)
|
||||
}
|
||||
|
||||
when (page) {
|
||||
SettingsPage.Root -> settingsRootContent(
|
||||
isTablet = false,
|
||||
onPlaybackClick = { onPageChange(SettingsPage.Playback) },
|
||||
onAppearanceClick = { onPageChange(SettingsPage.Appearance) },
|
||||
onNotificationsClick = { onPageChange(SettingsPage.Notifications) },
|
||||
onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
|
||||
onIntegrationsClick = { onPageChange(SettingsPage.Integrations) },
|
||||
onTraktClick = { onPageChange(SettingsPage.TraktAuthentication) },
|
||||
onSupportersContributorsClick = onSupportersContributorsClick,
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onDownloadsClick = onDownloadsClick,
|
||||
onAccountClick = onAccountClick,
|
||||
onSwitchProfileClick = onSwitchProfile,
|
||||
)
|
||||
SettingsPage.Account -> accountSettingsContent(
|
||||
isTablet = false,
|
||||
)
|
||||
SettingsPage.SupportersContributors -> supportersContributorsContent(
|
||||
isTablet = false,
|
||||
)
|
||||
SettingsPage.Playback -> playbackSettingsContent(
|
||||
isTablet = false,
|
||||
showLoadingOverlay = showLoadingOverlay,
|
||||
holdToSpeedEnabled = holdToSpeedEnabled,
|
||||
holdToSpeedValue = holdToSpeedValue,
|
||||
preferredAudioLanguage = preferredAudioLanguage,
|
||||
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
||||
preferredSubtitleLanguage = preferredSubtitleLanguage,
|
||||
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
|
||||
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
|
||||
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
|
||||
decoderPriority = decoderPriority,
|
||||
mapDV7ToHevc = mapDV7ToHevc,
|
||||
tunnelingEnabled = tunnelingEnabled,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
)
|
||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||
isTablet = false,
|
||||
selectedTheme = selectedTheme,
|
||||
onThemeSelected = onThemeSelected,
|
||||
amoledEnabled = amoledEnabled,
|
||||
onAmoledToggle = onAmoledToggle,
|
||||
selectedAppLanguage = selectedAppLanguage,
|
||||
onAppLanguageSelected = onAppLanguageSelected,
|
||||
onContinueWatchingClick = onContinueWatchingClick,
|
||||
onPosterCustomizationClick = { onPageChange(SettingsPage.PosterCustomization) },
|
||||
)
|
||||
SettingsPage.Notifications -> notificationsSettingsContent(
|
||||
isTablet = false,
|
||||
uiState = episodeReleaseNotificationsUiState,
|
||||
)
|
||||
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
||||
isTablet = false,
|
||||
isVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
style = continueWatchingPreferencesUiState.style,
|
||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||
)
|
||||
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
||||
isTablet = false,
|
||||
uiState = posterCardStyleUiState,
|
||||
)
|
||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||
isTablet = false,
|
||||
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
||||
onAddonsClick = onAddonsClick,
|
||||
onPluginsClick = onPluginsClick,
|
||||
onHomescreenClick = onHomescreenClick,
|
||||
onMetaScreenClick = onMetaScreenClick,
|
||||
onCollectionsClick = onCollectionsClick,
|
||||
)
|
||||
SettingsPage.Addons -> addonsSettingsContent()
|
||||
SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
|
||||
SettingsPage.Homescreen -> homescreenSettingsContent(
|
||||
isTablet = false,
|
||||
heroEnabled = homescreenHeroEnabled,
|
||||
items = homescreenItems,
|
||||
)
|
||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||
isTablet = false,
|
||||
uiState = metaScreenSettingsUiState,
|
||||
)
|
||||
SettingsPage.Integrations -> integrationsContent(
|
||||
isTablet = false,
|
||||
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
|
||||
onMdbListClick = { onPageChange(SettingsPage.MdbListRatings) },
|
||||
)
|
||||
SettingsPage.TmdbEnrichment -> tmdbSettingsContent(
|
||||
isTablet = false,
|
||||
settings = tmdbSettings,
|
||||
)
|
||||
SettingsPage.MdbListRatings -> mdbListSettingsContent(
|
||||
isTablet = false,
|
||||
settings = mdbListSettings,
|
||||
)
|
||||
SettingsPage.TraktAuthentication -> traktSettingsContent(
|
||||
isTablet = false,
|
||||
uiState = traktAuthUiState,
|
||||
commentsEnabled = traktCommentsEnabled,
|
||||
onCommentsEnabledChange = TraktCommentsSettings::setEnabled,
|
||||
)
|
||||
when (page) {
|
||||
SettingsPage.Root -> settingsRootContent(
|
||||
isTablet = false,
|
||||
onPlaybackClick = { onPageChange(SettingsPage.Playback) },
|
||||
onAppearanceClick = { onPageChange(SettingsPage.Appearance) },
|
||||
onNotificationsClick = { onPageChange(SettingsPage.Notifications) },
|
||||
onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
|
||||
onIntegrationsClick = { onPageChange(SettingsPage.Integrations) },
|
||||
onTraktClick = { onPageChange(SettingsPage.TraktAuthentication) },
|
||||
onSupportersContributorsClick = onSupportersContributorsClick,
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onDownloadsClick = onDownloadsClick,
|
||||
onAccountClick = onAccountClick,
|
||||
onSwitchProfileClick = onSwitchProfile,
|
||||
)
|
||||
SettingsPage.Account -> accountSettingsContent(
|
||||
isTablet = false,
|
||||
)
|
||||
SettingsPage.SupportersContributors -> supportersContributorsContent(
|
||||
isTablet = false,
|
||||
)
|
||||
SettingsPage.Playback -> playbackSettingsContent(
|
||||
isTablet = false,
|
||||
showLoadingOverlay = showLoadingOverlay,
|
||||
holdToSpeedEnabled = holdToSpeedEnabled,
|
||||
holdToSpeedValue = holdToSpeedValue,
|
||||
preferredAudioLanguage = preferredAudioLanguage,
|
||||
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
||||
preferredSubtitleLanguage = preferredSubtitleLanguage,
|
||||
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
|
||||
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
|
||||
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
|
||||
decoderPriority = decoderPriority,
|
||||
mapDV7ToHevc = mapDV7ToHevc,
|
||||
tunnelingEnabled = tunnelingEnabled,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
)
|
||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||
isTablet = false,
|
||||
selectedTheme = selectedTheme,
|
||||
onThemeSelected = onThemeSelected,
|
||||
amoledEnabled = amoledEnabled,
|
||||
onAmoledToggle = onAmoledToggle,
|
||||
selectedAppLanguage = selectedAppLanguage,
|
||||
onAppLanguageSelected = onAppLanguageSelected,
|
||||
onContinueWatchingClick = onContinueWatchingClick,
|
||||
onPosterCustomizationClick = { onPageChange(SettingsPage.PosterCustomization) },
|
||||
)
|
||||
SettingsPage.Notifications -> notificationsSettingsContent(
|
||||
isTablet = false,
|
||||
uiState = episodeReleaseNotificationsUiState,
|
||||
)
|
||||
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
||||
isTablet = false,
|
||||
isVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
style = continueWatchingPreferencesUiState.style,
|
||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||
)
|
||||
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
||||
isTablet = false,
|
||||
uiState = posterCardStyleUiState,
|
||||
)
|
||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||
isTablet = false,
|
||||
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
||||
onAddonsClick = onAddonsClick,
|
||||
onPluginsClick = onPluginsClick,
|
||||
onHomescreenClick = onHomescreenClick,
|
||||
onMetaScreenClick = onMetaScreenClick,
|
||||
onCollectionsClick = onCollectionsClick,
|
||||
)
|
||||
SettingsPage.Addons -> addonsSettingsContent()
|
||||
SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
|
||||
SettingsPage.Homescreen -> homescreenSettingsContent(
|
||||
isTablet = false,
|
||||
heroEnabled = homescreenHeroEnabled,
|
||||
items = homescreenItems,
|
||||
)
|
||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||
isTablet = false,
|
||||
uiState = metaScreenSettingsUiState,
|
||||
)
|
||||
SettingsPage.Integrations -> integrationsContent(
|
||||
isTablet = false,
|
||||
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
|
||||
onMdbListClick = { onPageChange(SettingsPage.MdbListRatings) },
|
||||
)
|
||||
SettingsPage.TmdbEnrichment -> tmdbSettingsContent(
|
||||
isTablet = false,
|
||||
settings = tmdbSettings,
|
||||
)
|
||||
SettingsPage.MdbListRatings -> mdbListSettingsContent(
|
||||
isTablet = false,
|
||||
settings = mdbListSettings,
|
||||
)
|
||||
SettingsPage.TraktAuthentication -> traktSettingsContent(
|
||||
isTablet = false,
|
||||
uiState = traktAuthUiState,
|
||||
commentsEnabled = traktCommentsEnabled,
|
||||
onCommentsEnabledChange = TraktCommentsSettings::setEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -468,6 +473,8 @@ private fun TabletSettingsScreen(
|
|||
onPageChange(page)
|
||||
}
|
||||
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
|
|
@ -510,134 +517,138 @@ private fun TabletSettingsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 40.dp,
|
||||
top = topOffset,
|
||||
end = 40.dp,
|
||||
bottom = 40.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||
) {
|
||||
item {
|
||||
val previousPage = page.previousPage()
|
||||
TabletPageHeader(
|
||||
title = if (page == SettingsPage.Root) {
|
||||
stringResource(activeCategory.labelRes)
|
||||
} else {
|
||||
stringResource(page.titleRes)
|
||||
},
|
||||
showBack = previousPage != null,
|
||||
onBack = { previousPage?.let(onPageChange) },
|
||||
)
|
||||
}
|
||||
when (page) {
|
||||
SettingsPage.Root -> settingsRootContent(
|
||||
isTablet = true,
|
||||
onPlaybackClick = { openInlinePage(SettingsPage.Playback) },
|
||||
onAppearanceClick = { openInlinePage(SettingsPage.Appearance) },
|
||||
onNotificationsClick = { openInlinePage(SettingsPage.Notifications) },
|
||||
onContentDiscoveryClick = { openInlinePage(SettingsPage.ContentDiscovery) },
|
||||
onIntegrationsClick = { openInlinePage(SettingsPage.Integrations) },
|
||||
onTraktClick = { openInlinePage(SettingsPage.TraktAuthentication) },
|
||||
onSupportersContributorsClick = { openInlinePage(SettingsPage.SupportersContributors) },
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onDownloadsClick = onDownloadsClick,
|
||||
onAccountClick = { openInlinePage(SettingsPage.Account) },
|
||||
onSwitchProfileClick = onSwitchProfile,
|
||||
showAccountSection = activeCategory == SettingsCategory.Account,
|
||||
showGeneralSection = activeCategory == SettingsCategory.General,
|
||||
showAboutSection = activeCategory == SettingsCategory.About,
|
||||
)
|
||||
SettingsPage.Account -> accountSettingsContent(
|
||||
isTablet = true,
|
||||
)
|
||||
SettingsPage.SupportersContributors -> supportersContributorsContent(
|
||||
isTablet = true,
|
||||
)
|
||||
SettingsPage.Playback -> playbackSettingsContent(
|
||||
isTablet = true,
|
||||
showLoadingOverlay = showLoadingOverlay,
|
||||
holdToSpeedEnabled = holdToSpeedEnabled,
|
||||
holdToSpeedValue = holdToSpeedValue,
|
||||
preferredAudioLanguage = preferredAudioLanguage,
|
||||
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
||||
preferredSubtitleLanguage = preferredSubtitleLanguage,
|
||||
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
|
||||
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
|
||||
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
|
||||
decoderPriority = decoderPriority,
|
||||
mapDV7ToHevc = mapDV7ToHevc,
|
||||
tunnelingEnabled = tunnelingEnabled,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
)
|
||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||
isTablet = true,
|
||||
selectedTheme = selectedTheme,
|
||||
onThemeSelected = onThemeSelected,
|
||||
amoledEnabled = amoledEnabled,
|
||||
onAmoledToggle = onAmoledToggle,
|
||||
selectedAppLanguage = selectedAppLanguage,
|
||||
onAppLanguageSelected = onAppLanguageSelected,
|
||||
onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) },
|
||||
onPosterCustomizationClick = { openInlinePage(SettingsPage.PosterCustomization) },
|
||||
)
|
||||
SettingsPage.Notifications -> notificationsSettingsContent(
|
||||
isTablet = true,
|
||||
uiState = episodeReleaseNotificationsUiState,
|
||||
)
|
||||
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
||||
isTablet = true,
|
||||
isVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
style = continueWatchingPreferencesUiState.style,
|
||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||
)
|
||||
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
||||
isTablet = true,
|
||||
uiState = posterCardStyleUiState,
|
||||
)
|
||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||
isTablet = true,
|
||||
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
||||
onAddonsClick = { openInlinePage(SettingsPage.Addons) },
|
||||
onPluginsClick = { openInlinePage(SettingsPage.Plugins) },
|
||||
onHomescreenClick = { openInlinePage(SettingsPage.Homescreen) },
|
||||
onMetaScreenClick = { openInlinePage(SettingsPage.MetaScreen) },
|
||||
onCollectionsClick = onCollectionsClick,
|
||||
)
|
||||
SettingsPage.Addons -> addonsSettingsContent()
|
||||
SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
|
||||
SettingsPage.Homescreen -> homescreenSettingsContent(
|
||||
isTablet = true,
|
||||
heroEnabled = homescreenHeroEnabled,
|
||||
items = homescreenItems,
|
||||
)
|
||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||
isTablet = true,
|
||||
uiState = metaScreenSettingsUiState,
|
||||
)
|
||||
SettingsPage.Integrations -> integrationsContent(
|
||||
isTablet = true,
|
||||
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
|
||||
onMdbListClick = { onPageChange(SettingsPage.MdbListRatings) },
|
||||
)
|
||||
SettingsPage.TmdbEnrichment -> tmdbSettingsContent(
|
||||
isTablet = true,
|
||||
settings = tmdbSettings,
|
||||
)
|
||||
SettingsPage.MdbListRatings -> mdbListSettingsContent(
|
||||
isTablet = true,
|
||||
settings = mdbListSettings,
|
||||
)
|
||||
SettingsPage.TraktAuthentication -> traktSettingsContent(
|
||||
isTablet = true,
|
||||
uiState = traktAuthUiState,
|
||||
commentsEnabled = traktCommentsEnabled,
|
||||
onCommentsEnabledChange = TraktCommentsSettings::setEnabled,
|
||||
)
|
||||
saveableStateHolder.SaveableStateProvider(page.name) {
|
||||
val listState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 40.dp,
|
||||
top = topOffset,
|
||||
end = 40.dp,
|
||||
bottom = 40.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||
) {
|
||||
item {
|
||||
val previousPage = page.previousPage()
|
||||
TabletPageHeader(
|
||||
title = if (page == SettingsPage.Root) {
|
||||
stringResource(activeCategory.labelRes)
|
||||
} else {
|
||||
stringResource(page.titleRes)
|
||||
},
|
||||
showBack = previousPage != null,
|
||||
onBack = { previousPage?.let(onPageChange) },
|
||||
)
|
||||
}
|
||||
when (page) {
|
||||
SettingsPage.Root -> settingsRootContent(
|
||||
isTablet = true,
|
||||
onPlaybackClick = { openInlinePage(SettingsPage.Playback) },
|
||||
onAppearanceClick = { openInlinePage(SettingsPage.Appearance) },
|
||||
onNotificationsClick = { openInlinePage(SettingsPage.Notifications) },
|
||||
onContentDiscoveryClick = { openInlinePage(SettingsPage.ContentDiscovery) },
|
||||
onIntegrationsClick = { openInlinePage(SettingsPage.Integrations) },
|
||||
onTraktClick = { openInlinePage(SettingsPage.TraktAuthentication) },
|
||||
onSupportersContributorsClick = { openInlinePage(SettingsPage.SupportersContributors) },
|
||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||
onDownloadsClick = onDownloadsClick,
|
||||
onAccountClick = { openInlinePage(SettingsPage.Account) },
|
||||
onSwitchProfileClick = onSwitchProfile,
|
||||
showAccountSection = activeCategory == SettingsCategory.Account,
|
||||
showGeneralSection = activeCategory == SettingsCategory.General,
|
||||
showAboutSection = activeCategory == SettingsCategory.About,
|
||||
)
|
||||
SettingsPage.Account -> accountSettingsContent(
|
||||
isTablet = true,
|
||||
)
|
||||
SettingsPage.SupportersContributors -> supportersContributorsContent(
|
||||
isTablet = true,
|
||||
)
|
||||
SettingsPage.Playback -> playbackSettingsContent(
|
||||
isTablet = true,
|
||||
showLoadingOverlay = showLoadingOverlay,
|
||||
holdToSpeedEnabled = holdToSpeedEnabled,
|
||||
holdToSpeedValue = holdToSpeedValue,
|
||||
preferredAudioLanguage = preferredAudioLanguage,
|
||||
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
||||
preferredSubtitleLanguage = preferredSubtitleLanguage,
|
||||
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
|
||||
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
|
||||
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
|
||||
decoderPriority = decoderPriority,
|
||||
mapDV7ToHevc = mapDV7ToHevc,
|
||||
tunnelingEnabled = tunnelingEnabled,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
)
|
||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||
isTablet = true,
|
||||
selectedTheme = selectedTheme,
|
||||
onThemeSelected = onThemeSelected,
|
||||
amoledEnabled = amoledEnabled,
|
||||
onAmoledToggle = onAmoledToggle,
|
||||
selectedAppLanguage = selectedAppLanguage,
|
||||
onAppLanguageSelected = onAppLanguageSelected,
|
||||
onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) },
|
||||
onPosterCustomizationClick = { openInlinePage(SettingsPage.PosterCustomization) },
|
||||
)
|
||||
SettingsPage.Notifications -> notificationsSettingsContent(
|
||||
isTablet = true,
|
||||
uiState = episodeReleaseNotificationsUiState,
|
||||
)
|
||||
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
||||
isTablet = true,
|
||||
isVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
style = continueWatchingPreferencesUiState.style,
|
||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||
)
|
||||
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
||||
isTablet = true,
|
||||
uiState = posterCardStyleUiState,
|
||||
)
|
||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||
isTablet = true,
|
||||
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
||||
onAddonsClick = { openInlinePage(SettingsPage.Addons) },
|
||||
onPluginsClick = { openInlinePage(SettingsPage.Plugins) },
|
||||
onHomescreenClick = { openInlinePage(SettingsPage.Homescreen) },
|
||||
onMetaScreenClick = { openInlinePage(SettingsPage.MetaScreen) },
|
||||
onCollectionsClick = onCollectionsClick,
|
||||
)
|
||||
SettingsPage.Addons -> addonsSettingsContent()
|
||||
SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
|
||||
SettingsPage.Homescreen -> homescreenSettingsContent(
|
||||
isTablet = true,
|
||||
heroEnabled = homescreenHeroEnabled,
|
||||
items = homescreenItems,
|
||||
)
|
||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||
isTablet = true,
|
||||
uiState = metaScreenSettingsUiState,
|
||||
)
|
||||
SettingsPage.Integrations -> integrationsContent(
|
||||
isTablet = true,
|
||||
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
|
||||
onMdbListClick = { onPageChange(SettingsPage.MdbListRatings) },
|
||||
)
|
||||
SettingsPage.TmdbEnrichment -> tmdbSettingsContent(
|
||||
isTablet = true,
|
||||
settings = tmdbSettings,
|
||||
)
|
||||
SettingsPage.MdbListRatings -> mdbListSettingsContent(
|
||||
isTablet = true,
|
||||
settings = mdbListSettings,
|
||||
)
|
||||
SettingsPage.TraktAuthentication -> traktSettingsContent(
|
||||
isTablet = true,
|
||||
uiState = traktAuthUiState,
|
||||
commentsEnabled = traktCommentsEnabled,
|
||||
onCommentsEnabledChange = TraktCommentsSettings::setEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue