mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-18 16:01:44 +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 kotlinx.coroutines.withContext
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
private const val TAG = "NuvioPlayer"
|
private const val TAG = "NuvioPlayer"
|
||||||
|
|
||||||
|
|
@ -180,6 +179,10 @@ actual fun PlatformPlayerSurface(
|
||||||
var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) }
|
var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) }
|
||||||
var subtitleSelectionJob by remember { mutableStateOf<Job?>(null) }
|
var subtitleSelectionJob by remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
|
fun syncPlayerViewKeepScreenOn() {
|
||||||
|
playerViewRef?.keepScreenOn = exoPlayer.shouldKeepPlayerScreenOn()
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(exoPlayer) {
|
DisposableEffect(exoPlayer) {
|
||||||
PlayerPictureInPictureManager.registerPausePlaybackCallback {
|
PlayerPictureInPictureManager.registerPausePlaybackCallback {
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
|
|
@ -187,6 +190,7 @@ actual fun PlatformPlayerSurface(
|
||||||
|
|
||||||
val listener = object : Player.Listener {
|
val listener = object : Player.Listener {
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
|
syncPlayerViewKeepScreenOn()
|
||||||
latestOnError.value(error.localizedMessage ?: runBlocking { getString(Res.string.player_unable_to_play_stream) })
|
latestOnError.value(error.localizedMessage ?: runBlocking { getString(Res.string.player_unable_to_play_stream) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,10 +207,12 @@ actual fun PlatformPlayerSurface(
|
||||||
latestOnError.value(null)
|
latestOnError.value(null)
|
||||||
exoPlayer.logCurrentTracks("STATE_READY")
|
exoPlayer.logCurrentTracks("STATE_READY")
|
||||||
}
|
}
|
||||||
|
syncPlayerViewKeepScreenOn()
|
||||||
latestOnSnapshot.value(exoPlayer.snapshot())
|
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
syncPlayerViewKeepScreenOn()
|
||||||
latestOnSnapshot.value(exoPlayer.snapshot())
|
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,6 +242,7 @@ actual fun PlatformPlayerSurface(
|
||||||
onDispose {
|
onDispose {
|
||||||
PlayerPictureInPictureManager.registerPausePlaybackCallback(null)
|
PlayerPictureInPictureManager.registerPausePlaybackCallback(null)
|
||||||
exoPlayer.removeListener(listener)
|
exoPlayer.removeListener(listener)
|
||||||
|
playerViewRef?.keepScreenOn = false
|
||||||
subtitleSelectionJob?.cancel()
|
subtitleSelectionJob?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,6 +272,7 @@ actual fun PlatformPlayerSurface(
|
||||||
|
|
||||||
LaunchedEffect(exoPlayer, playWhenReady) {
|
LaunchedEffect(exoPlayer, playWhenReady) {
|
||||||
exoPlayer.playWhenReady = playWhenReady
|
exoPlayer.playWhenReady = playWhenReady
|
||||||
|
syncPlayerViewKeepScreenOn()
|
||||||
latestOnSnapshot.value(exoPlayer.snapshot())
|
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,10 +306,10 @@ actual fun PlatformPlayerSurface(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioTracks(): List<AudioTrack> =
|
override fun getAudioTracks(): List<AudioTrack> =
|
||||||
exoPlayer.extractAudioTracks()
|
exoPlayer.extractAudioTracks(context)
|
||||||
|
|
||||||
override fun getSubtitleTracks(): List<SubtitleTrack> {
|
override fun getSubtitleTracks(): List<SubtitleTrack> {
|
||||||
val tracks = exoPlayer.extractSubtitleTracks()
|
val tracks = exoPlayer.extractSubtitleTracks(context)
|
||||||
Log.d(TAG, "getSubtitleTracks: found ${tracks.size} tracks")
|
Log.d(TAG, "getSubtitleTracks: found ${tracks.size} tracks")
|
||||||
tracks.forEach { t ->
|
tracks.forEach { t ->
|
||||||
Log.d(TAG, " track idx=${t.index} id=${t.id} label='${t.label}' lang=${t.language} selected=${t.isSelected}")
|
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
|
useController = useNativeController
|
||||||
layoutParams = android.view.ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
layoutParams = android.view.ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
player = exoPlayer
|
player = exoPlayer
|
||||||
keepScreenOn = true
|
keepScreenOn = exoPlayer.shouldKeepPlayerScreenOn()
|
||||||
this.resizeMode = resizeMode.toExoResizeMode()
|
this.resizeMode = resizeMode.toExoResizeMode()
|
||||||
setShutterBackgroundColor(android.graphics.Color.BLACK)
|
setShutterBackgroundColor(android.graphics.Color.BLACK)
|
||||||
playerViewRef = this
|
playerViewRef = this
|
||||||
|
|
@ -442,6 +450,7 @@ actual fun PlatformPlayerSurface(
|
||||||
playerView.useController = useNativeController
|
playerView.useController = useNativeController
|
||||||
playerView.resizeMode = resizeMode.toExoResizeMode()
|
playerView.resizeMode = resizeMode.toExoResizeMode()
|
||||||
playerViewRef = playerView
|
playerViewRef = playerView
|
||||||
|
syncPlayerViewKeepScreenOn()
|
||||||
playerView.syncLibassOverlay(
|
playerView.syncLibassOverlay(
|
||||||
player = exoPlayer,
|
player = exoPlayer,
|
||||||
enabled = useLibass,
|
enabled = useLibass,
|
||||||
|
|
@ -470,6 +479,11 @@ private fun ExoPlayer.snapshot(): PlayerPlaybackSnapshot =
|
||||||
playbackSpeed = playbackParameters.speed,
|
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 =
|
private fun PlayerResizeMode.toExoResizeMode(): Int =
|
||||||
when (this) {
|
when (this) {
|
||||||
PlayerResizeMode.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
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 tracks = mutableListOf<AudioTrack>()
|
||||||
|
val trackNameProvider = CustomDefaultTrackNameProvider(context.resources)
|
||||||
var idx = 0
|
var idx = 0
|
||||||
for (group in currentTracks.groups) {
|
for (group in currentTracks.groups) {
|
||||||
if (group.type != C.TRACK_TYPE_AUDIO) continue
|
if (group.type != C.TRACK_TYPE_AUDIO) continue
|
||||||
val format = group.mediaTrackGroup.getFormat(0)
|
val format = group.mediaTrackGroup.getFormat(0)
|
||||||
val channelLabel = when {
|
val label = trackNameProvider.getTrackName(format).takeIf { it.isNotBlank() }
|
||||||
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
|
|
||||||
?: runBlocking { getString(Res.string.compose_player_track_number, idx + 1) }
|
?: runBlocking { getString(Res.string.compose_player_track_number, idx + 1) }
|
||||||
val suffix = listOfNotNull(channelLabel, codecLabel)
|
|
||||||
.joinToString(" ")
|
|
||||||
.let { if (it.isNotBlank()) " ($it)" else "" }
|
|
||||||
tracks.add(
|
tracks.add(
|
||||||
AudioTrack(
|
AudioTrack(
|
||||||
index = idx,
|
index = idx,
|
||||||
id = format.id ?: idx.toString(),
|
id = format.id ?: idx.toString(),
|
||||||
label = "$baseName$suffix",
|
label = label,
|
||||||
language = format.language,
|
language = format.language,
|
||||||
isSelected = group.isSelected,
|
isSelected = group.isSelected,
|
||||||
)
|
)
|
||||||
|
|
@ -609,8 +596,9 @@ private fun ExoPlayer.extractAudioTracks(): List<AudioTrack> {
|
||||||
return tracks
|
return tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ExoPlayer.extractSubtitleTracks(): List<SubtitleTrack> {
|
private fun ExoPlayer.extractSubtitleTracks(context: Context): List<SubtitleTrack> {
|
||||||
val tracks = mutableListOf<SubtitleTrack>()
|
val tracks = mutableListOf<SubtitleTrack>()
|
||||||
|
val trackNameProvider = CustomDefaultTrackNameProvider(context.resources)
|
||||||
var idx = 0
|
var idx = 0
|
||||||
for (group in currentTracks.groups) {
|
for (group in currentTracks.groups) {
|
||||||
if (group.type != C.TRACK_TYPE_TEXT) continue
|
if (group.type != C.TRACK_TYPE_TEXT) continue
|
||||||
|
|
@ -620,7 +608,7 @@ private fun ExoPlayer.extractSubtitleTracks(): List<SubtitleTrack> {
|
||||||
SubtitleTrack(
|
SubtitleTrack(
|
||||||
index = idx,
|
index = idx,
|
||||||
id = format.id ?: idx.toString(),
|
id = format.id ?: idx.toString(),
|
||||||
label = format.label ?: "",
|
label = trackNameProvider.getTrackName(format),
|
||||||
language = format.language,
|
language = format.language,
|
||||||
isSelected = group.isSelected,
|
isSelected = group.isSelected,
|
||||||
isForced = inferForcedSubtitleTrack(
|
isForced = inferForcedSubtitleTrack(
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ actual fun LockPlayerToLandscape() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun EnterImmersivePlayerMode() {
|
actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) {
|
||||||
val activity = LocalContext.current.findActivity() ?: return
|
val activity = LocalContext.current.findActivity() ?: return
|
||||||
|
|
||||||
DisposableEffect(activity) {
|
DisposableEffect(activity) {
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@
|
||||||
<locale android:name="it"/>
|
<locale android:name="it"/>
|
||||||
<locale android:name="el"/>
|
<locale android:name="el"/>
|
||||||
<locale android:name="pl"/>
|
<locale android:name="pl"/>
|
||||||
|
<locale android:name="de"/>
|
||||||
</locale-config>
|
</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>
|
<resources>
|
||||||
<string name="about_supporters_contributors_subtitle">Reconnaissance et crédits du projet</string>
|
<string name="about_supporters_contributors_subtitle">Reconnaissance et crédits du projet</string>
|
||||||
<string name="action_back">Retour</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_production_mode">Production</string>
|
||||||
<string name="collections_editor_tmdb_network_mode">Chaîne</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_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_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_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_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_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_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_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_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_public_list">Liste publique TMDB</string>
|
||||||
<string name="collections_editor_tmdb_network_id">ID de chaîne</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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_search_results">Résultats de recherche</string>
|
||||||
<string name="collections_editor_tmdb_collection">Collection TMDB</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_presets">Préréglages</string>
|
||||||
<string name="collections_editor_tmdb_search">Rechercher</string>
|
<string name="collections_editor_tmdb_search">Rechercher</string>
|
||||||
<string name="collections_editor_add_source">Ajouter une source</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_action">Action</string>
|
||||||
<string name="collections_editor_tmdb_genre_adventure">Aventure</string>
|
<string name="collections_editor_tmdb_genre_adventure">Aventure</string>
|
||||||
<string name="collections_editor_tmdb_genre_animation">Animation</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_disney_plus">Disney+</string>
|
||||||
<string name="collections_editor_tmdb_network_prime_video">Prime Video</string>
|
<string name="collections_editor_tmdb_network_prime_video">Prime Video</string>
|
||||||
<string name="collections_editor_tmdb_network_hulu">Hulu</string>
|
<string name="collections_editor_tmdb_network_hulu">Hulu</string>
|
||||||
|
<string name="collections_editor_tmdb_sort_original">Original</string>
|
||||||
<string name="collections_editor_tmdb_sort_popular">Populaire</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_top_rated">Mieux notés</string>
|
||||||
<string name="collections_editor_tmdb_sort_recent">Récent</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_movie_collection">Collection de films TMDB</string>
|
||||||
<string name="collections_editor_tmdb_subtitle_production">Production</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_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_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_subtitle">Créez-en une pour organiser vos catalogues.</string>
|
||||||
<string name="collections_empty_title">Aucune collection</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_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_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_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_store_locally">Vos données seront uniquement stockées localement</string>
|
||||||
<string name="compose_auth_tagline">Regardez tout, partout</string>
|
<string name="compose_auth_tagline">Regardez tout, partout</string>
|
||||||
<string name="compose_auth_welcome_back">Bon retour</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_profile_add_profile">Ajouter un profil</string>
|
||||||
<string name="compose_search_clear">Effacer la recherche</string>
|
<string name="compose_search_clear">Effacer la recherche</string>
|
||||||
<string name="compose_search_discover_title">Découvrir</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_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_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_active_addons_title">Aucun addon actif</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_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_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_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>
|
<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_homescreen_visible">Visible</string>
|
||||||
<string name="settings_playback_subtitle">Lecteur, sous-titres et lecture automatique</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_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_card_width">Largeur de carte</string>
|
||||||
<string name="settings_poster_custom">Personnalisé</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>
|
<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="settings_notifications_test_title">Notification de test</string>
|
||||||
<string name="community_section_title">Communauté</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_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_contributors">Contributeurs</string>
|
||||||
<string name="community_tab_supporters">Supporters</string>
|
<string name="community_tab_supporters">Supporters</string>
|
||||||
<string name="community_open_github">Ouvrir GitHub</string>
|
<string name="community_open_github">Ouvrir GitHub</string>
|
||||||
|
|
@ -582,7 +614,7 @@
|
||||||
<string name="community_month_nov">Nov</string>
|
<string name="community_month_nov">Nov</string>
|
||||||
<string name="community_month_dec">Déc</string>
|
<string name="community_month_dec">Déc</string>
|
||||||
<string name="community_date_format">%1$s %2$s %3$s</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_all_plugins">Tous les plugins</string>
|
||||||
<string name="settings_playback_allowed_addons">Addons autorisés</string>
|
<string name="settings_playback_allowed_addons">Addons autorisés</string>
|
||||||
<string name="settings_playback_allowed_plugins">Plugins 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_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_pattern">Modèle regex</string>
|
||||||
<string name="settings_playback_regex_placeholder">4K|2160p|Remux</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_avc_x264">AVC / x264</string>
|
||||||
<string name="settings_playback_regex_preset_bluray_quality">Qualité BluRay</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>
|
<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">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_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">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">Tous 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_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">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_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>
|
<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="action_yes">Oui</string>
|
||||||
<string name="app_exit_message">Voulez-vous quitter l'application ?</string>
|
<string name="app_exit_message">Voulez-vous quitter l'application ?</string>
|
||||||
<string name="app_exit_title">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="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_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_director">Réalisateur</string>
|
||||||
<string name="details_failed_to_load">Échec du chargement</string>
|
<string name="details_failed_to_load">Échec du chargement</string>
|
||||||
<string name="details_more_like_this">Plus comme ceci</string>
|
<string name="details_more_like_this">Plus comme ceci</string>
|
||||||
<string name="details_seasons">Saisons</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_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_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>
|
<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_all_genres">Tous les genres</string>
|
||||||
<string name="discover_catalog">Catalogue</string>
|
<string name="discover_catalog">Catalogue</string>
|
||||||
<string name="discover_catalog_context">%1$s • %2$s</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_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_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_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_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_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>
|
<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_select_avatar">Sélectionnez un avatar pour ce profil.</string>
|
||||||
<string name="profile_set_pin_lock">Configurer le verrouillage PIN</string>
|
<string name="profile_set_pin_lock">Configurer le verrouillage PIN</string>
|
||||||
<string name="profile_unnamed">Profil sans nom</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_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="profile_who_is_watching">Qui regarde ?</string>
|
||||||
<string name="provider_downloaded">Téléchargé</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_checking_more_addons">Vérification d'autres addons…</string>
|
||||||
<string name="streams_copy_link">Copier le lien du stream</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_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_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_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_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_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_badge">S%1$d E%2$d</string>
|
||||||
<string name="streams_episode_fallback_title">Épisode</string>
|
<string name="streams_episode_fallback_title">Épisode</string>
|
||||||
<string name="streams_episode_title_with_name">S%1$dE%2$d - %3$s</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_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_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_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="streams_failed_to_load_scraper">Impossible de charger %1$s</string>
|
||||||
<string name="stream_default_name">Source</string>
|
<string name="stream_default_name">Source</string>
|
||||||
<string name="source_embedded">Intégré</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_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_library_load_failed">Impossible de charger la bibliothèque Trakt</string>
|
||||||
<string name="trakt_list_fallback_title">Liste %1$d</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_missing_credentials">Identifiants Trakt manquants</string>
|
||||||
<string name="trakt_progress_load_failed">Impossible de charger la progression Trakt</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>
|
<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_play_episode">Lire %1$s</string>
|
||||||
<string name="action_resume_episode">Reprendre %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_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_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_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_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_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_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_import_error_invalid_json">JSON invalide : %1$s</string>
|
||||||
<string name="collections_folder_addon_not_found">Addon introuvable : %1$s</string>
|
<string name="collections_folder_addon_not_found">Addon introuvable : %1$s</string>
|
||||||
<string name="date_month_january">Janvier</string>
|
<string name="date_month_january">Janvier</string>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<string name="action_previous">Previous</string>
|
<string name="action_previous">Previous</string>
|
||||||
<string name="action_remove">Remove</string>
|
<string name="action_remove">Remove</string>
|
||||||
<string name="action_reorder">Reorder</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_resume">Resume</string>
|
||||||
<string name="action_retry">Retry</string>
|
<string name="action_retry">Retry</string>
|
||||||
<string name="action_save">Save</string>
|
<string name="action_save">Save</string>
|
||||||
|
|
@ -361,36 +361,36 @@
|
||||||
<string name="compose_settings_category_general">General</string>
|
<string name="compose_settings_category_general">General</string>
|
||||||
<string name="compose_settings_page_account">Account</string>
|
<string name="compose_settings_page_account">Account</string>
|
||||||
<string name="compose_settings_page_addons">Addons</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_content_discovery">Content & Discovery</string>
|
||||||
<string name="compose_settings_page_continue_watching">Continue Watching</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_integrations">Integrations</string>
|
||||||
<string name="compose_settings_page_mdblist_ratings">MDBList Ratings</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_notifications">Notifications</string>
|
||||||
<string name="compose_settings_page_playback">Playback</string>
|
<string name="compose_settings_page_playback">Playback</string>
|
||||||
<string name="compose_settings_page_plugins">Plugins</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_root">Settings</string>
|
||||||
<string name="compose_settings_page_supporters_contributors">Supporters & Contributors</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_tmdb_enrichment">TMDB Enrichment</string>
|
||||||
<string name="compose_settings_page_trakt">Trakt</string>
|
<string name="compose_settings_page_trakt">Trakt</string>
|
||||||
<string name="compose_settings_root_about_section">ABOUT</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_account_section">ACCOUNT</string>
|
||||||
<string name="compose_settings_root_appearance_description">Tune home presentation and visual preferences.</string>
|
<string name="compose_settings_root_appearance_description">Home structure and poster styles</string>
|
||||||
<string name="compose_settings_root_check_updates_description">Check for new versions of the app.</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_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_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_description">Manage your downloaded movies and episodes.</string>
|
||||||
<string name="compose_settings_root_downloads_title">Downloads</string>
|
<string name="compose_settings_root_downloads_title">Downloads</string>
|
||||||
<string name="compose_settings_root_general_section">GENERAL</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_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_description">Change to a different profile.</string>
|
||||||
<string name="compose_settings_root_switch_profile_title">Switch 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_loading">Loading your Trakt lists…</string>
|
||||||
<string name="compose_trakt_list_picker_subtitle">Choose where to save this title on Trakt</string>
|
<string name="compose_trakt_list_picker_subtitle">Choose where to save this title on Trakt</string>
|
||||||
<string name="action_donate">Donate</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_sign_out_confirm_title">Sign Out?</string>
|
||||||
<string name="settings_account_status">Status</string>
|
<string name="settings_account_status">Status</string>
|
||||||
<string name="settings_account_status_anonymous">Anonymous</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_black">AMOLED Black</string>
|
||||||
<string name="settings_appearance_amoled_description">Use pure black backgrounds for OLED screens.</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">App Language</string>
|
||||||
<string name="settings_appearance_app_language_sheet_title">Choose 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_continue_watching_description">Settings for the Continue Watching section.</string>
|
||||||
<string name="settings_appearance_poster_customization_description">Adjust shared poster card width and corner radius presets.</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_display">DISPLAY</string>
|
||||||
<string name="settings_appearance_section_home">HOME</string>
|
<string name="settings_appearance_section_home">HOME</string>
|
||||||
<string name="settings_appearance_section_theme">THEME</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">CATALOGS</string>
|
||||||
<string name="settings_homescreen_section_catalogs_collections">CATALOGS & COLLECTIONS</string>
|
<string name="settings_homescreen_section_catalogs_collections">CATALOGS & COLLECTIONS</string>
|
||||||
<string name="settings_homescreen_section_collections">COLLECTIONS</string>
|
<string name="settings_homescreen_section_collections">COLLECTIONS</string>
|
||||||
<string name="settings_homescreen_section_hero">HERO</string>
|
<string name="settings_homescreen_section_hero">Home Layout</string>
|
||||||
<string name="settings_homescreen_section_hero_sources">HERO SOURCES</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_selected_count">%1$d of %2$d selected</string>
|
||||||
<string name="settings_homescreen_show_hero">Show Hero</string>
|
<string name="settings_homescreen_show_hero">Show Hero Section</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_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">%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_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_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_playback_subtitle">Player, subtitles, and auto-play</string>
|
||||||
<string name="settings_poster_card_radius">Card Radius</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_style">Poster Card Style</string>
|
||||||
<string name="settings_poster_card_width">Card Width</string>
|
<string name="settings_poster_card_width">Width</string>
|
||||||
<string name="settings_poster_custom">Custom</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_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_live_preview">Live Preview</string>
|
||||||
<string name="settings_poster_option_with_value">%1$s (%2$s)</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>
|
<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_dense">Dense</string>
|
||||||
<string name="settings_poster_width_large">Large</string>
|
<string name="settings_poster_width_large">Large</string>
|
||||||
<string name="settings_poster_width_standard">Standard</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_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_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_on_launch">ON LAUNCH</string>
|
||||||
<string name="settings_continue_watching_section_up_next_behavior">UP NEXT BEHAVIOR</string>
|
<string name="settings_continue_watching_section_up_next_behavior">UP NEXT BEHAVIOR</string>
|
||||||
<string name="settings_continue_watching_section_visibility">VISIBILITY</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_poster_description">Artwork-first poster card</string>
|
||||||
<string name="settings_continue_watching_style_wide">Wide</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_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_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_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_home">HOME</string>
|
||||||
<string name="settings_content_discovery_section_sources">SOURCES</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_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_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_homescreen_description">Adjust home layout, content visibility, and poster behavior</string>
|
||||||
<string name="settings_content_discovery_meta_screen_description">Disable detail sections and reorder everything below Hero.</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_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_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_tmdb_description">Metadata enrichment controls</string>
|
||||||
<string name="settings_integrations_mdblist_description">Add IMDb, Rotten Tomatoes, Metacritic, and other external ratings to details pages.</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_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_description">Required to fetch ratings from MDBList</string>
|
||||||
<string name="settings_mdb_api_key_label">API key</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_api_key_title">API Key</string>
|
||||||
<string name="settings_mdb_enable_ratings">Enable MDBList ratings</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_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_api_key">API Key</string>
|
||||||
<string name="settings_mdb_section_rating_providers">RATING PROVIDERS</string>
|
<string name="settings_mdb_section_rating_providers">External ratings providers</string>
|
||||||
<string name="settings_mdb_section_title">MDBLIST</string>
|
<string name="settings_mdb_section_title">MDBList Ratings</string>
|
||||||
<string name="settings_meta_actions">Actions</string>
|
<string name="settings_meta_actions">Actions</string>
|
||||||
<string name="settings_meta_actions_description">Play and save controls.</string>
|
<string name="settings_meta_actions_description">Play and save controls.</string>
|
||||||
<string name="settings_meta_cast">Cast</string>
|
<string name="settings_meta_cast">Cast</string>
|
||||||
|
|
@ -544,7 +546,7 @@
|
||||||
<string name="settings_meta_collection">Collection</string>
|
<string name="settings_meta_collection">Collection</string>
|
||||||
<string name="settings_meta_collection_description">Related collection or franchise rail.</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">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">Details</string>
|
||||||
<string name="settings_meta_details_description">Runtime, status, release, language, and related info.</string>
|
<string name="settings_meta_details_description">Runtime, status, release, language, and related info.</string>
|
||||||
<string name="settings_meta_episode_cards">Episode Cards</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">Episodes</string>
|
||||||
<string name="settings_meta_episodes_description">Seasons and episode list for series.</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_group_label">Group %1$d</string>
|
||||||
<string name="settings_meta_more_like_this">More Like This</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_description">TMDB recommendation backdrops on detail page</string>
|
||||||
<string name="settings_meta_none">None</string>
|
<string name="settings_meta_none">None</string>
|
||||||
<string name="settings_meta_overview">Overview</string>
|
<string name="settings_meta_overview">Overview</string>
|
||||||
<string name="settings_meta_overview_description">Synopsis, ratings, genres, and core credits.</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_nov">Nov</string>
|
||||||
<string name="community_month_dec">Dec</string>
|
<string name="community_month_dec">Dec</string>
|
||||||
<string name="community_date_format">%1$s %2$s, %3$s</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_addons">All installed addons</string>
|
||||||
<string name="settings_playback_all_plugins">All Plugins</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_addons">Allowed Addons</string>
|
||||||
<string name="settings_playback_allowed_plugins">Allowed Plugins</string>
|
<string name="settings_playback_allowed_plugins">Allowed Plugins</string>
|
||||||
<string name="settings_playback_anime_skip">Anime Skip</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">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_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_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">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_auto_play_next_episode_description">Start next episode automatically when prompt appears.</string>
|
||||||
<string name="settings_playback_decoder_device_only">Device Only</string>
|
<string name="settings_playback_decoder_device_only">Device decoders only</string>
|
||||||
<string name="settings_playback_decoder_prefer_app">Prefer App (FFmpeg)</string>
|
<string name="settings_playback_decoder_prefer_app">Prefer app decoders (FFmpeg)</string>
|
||||||
<string name="settings_playback_decoder_prefer_device">Prefer Device</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_decoder_priority">Decoder Priority</string>
|
||||||
<string name="settings_playback_dialog_close">Tap outside to close</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>
|
<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_days">%1$d days</string>
|
||||||
<string name="settings_playback_duration_hour_one">%1$d hour</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_duration_hours">%1$d hours</string>
|
||||||
<string name="settings_playback_enable_libass">Enable libass</string>
|
<string name="settings_playback_enable_libass">Use libass for ASS/SSA subtitles</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_description">Experimental: advanced ASS/SSA rendering (styles, positioning, animations)</string>
|
||||||
<string name="settings_playback_hold_speed">Hold Speed</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">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_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_invalid_regex_pattern">Invalid regex pattern</string>
|
||||||
<string name="settings_playback_last_link_cache_duration">Last Link Cache Duration</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">DV7 - HEVC Fallback</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_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">Minutes Before End</string>
|
<string name="settings_playback_minutes_before_end">Threshold Minutes</string>
|
||||||
<string name="settings_playback_minutes_before_end_description">Show next episode card this many minutes before the end.</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_minutes_value">%1$d min</string>
|
||||||
<string name="settings_playback_no_items_available">No items available</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_not_set">Not set</string>
|
||||||
<string name="settings_playback_option_default">Default</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_device_language">Device language</string>
|
||||||
<string name="settings_playback_option_forced">Forced</string>
|
<string name="settings_playback_option_forced">Forced</string>
|
||||||
<string name="settings_playback_option_none">None</string>
|
<string name="settings_playback_option_none">None</string>
|
||||||
<string name="settings_playback_prefer_binge_group">Prefer Binge Group</string>
|
<string name="settings_playback_prefer_binge_group">Prefer Binge Group (Next Episode)</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_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_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_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_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_any_1080p">Any 1080p+</string>
|
||||||
<string name="settings_playback_regex_preset_avc_x264">AVC / x264</string>
|
<string name="settings_playback_regex_preset_avc_x264">AVC / x264</string>
|
||||||
<string name="settings_playback_regex_preset_bluray_quality">BluRay Quality</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_4k_remux">4K / Remux</string>
|
||||||
<string name="settings_playback_regex_preset_quality_720p_smaller">720p / Smaller</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_regex_preset_web_sources">WEB Sources</string>
|
||||||
<string name="settings_playback_render_type">Render Type</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_cues">Standard Cues</string>
|
||||||
<string name="settings_playback_render_type_effects_canvas">Effects Canvas</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_effects_opengl">Effects OpenGL</string>
|
||||||
<string name="settings_playback_render_type_overlay_canvas">Overlay Canvas</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">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_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_decoder">DECODER</string>
|
||||||
<string name="settings_playback_section_next_episode">NEXT EPISODE</string>
|
<string name="settings_playback_section_next_episode">NEXT EPISODE</string>
|
||||||
<string name="settings_playback_section_player">PLAYER</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_audio">SUBTITLE AND AUDIO</string>
|
||||||
<string name="settings_playback_section_subtitle_rendering">SUBTITLE RENDERING</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_selected_count">%1$d selected</string>
|
||||||
<string name="settings_playback_show_loading_overlay">Show Loading Overlay</string>
|
<string name="settings_playback_show_loading_overlay">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_show_loading_overlay_description">Show loading screen until first video frame appears.</string>
|
||||||
<string name="settings_playback_skip_intro_outro_recap">Skip Intro/Outro/Recap</string>
|
<string name="settings_playback_skip_intro_outro_recap">Skip Intro</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_skip_intro_outro_recap_description">Use introdb.app to detect intros and recaps.</string>
|
||||||
<string name="settings_playback_source_scope">Source Scope</string>
|
<string name="settings_playback_source_scope">Auto-play Source Scope</string>
|
||||||
<string name="settings_playback_source_scope_all_addons">All Addons</string>
|
<string name="settings_playback_source_scope_all_addons">All installed 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_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">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_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">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_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">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_source_scope_installed_addons_only_description">Auto-play only considers streams coming from your installed addons.</string>
|
||||||
<string name="settings_playback_stream_selection_mode">Stream Selection Mode</string>
|
<string name="settings_playback_stream_selection_mode">Auto Stream Selection</string>
|
||||||
<string name="settings_playback_stream_selection_mode_first_stream">First Available Stream</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">Automatically play the first stream found.</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</string>
|
<string name="settings_playback_stream_selection_mode_manual">Manual (choose stream)</string>
|
||||||
<string name="settings_playback_stream_selection_mode_manual_description">Select streams manually each time.</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">Regex Match</string>
|
<string name="settings_playback_stream_selection_mode_regex">Auto-play 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_selection_mode_regex_description">Play first source whose text matches your regex pattern.</string>
|
||||||
<string name="settings_playback_stream_timeout">Stream Timeout</string>
|
<string name="settings_playback_stream_timeout">Stream Selection Timeout</string>
|
||||||
<string name="settings_playback_stream_timeout_description">How long to wait for streams before auto-selecting.</string>
|
<string name="settings_playback_stream_timeout_description">Wait time for addons before selecting.</string>
|
||||||
<string name="settings_playback_threshold_minutes">Minutes Before End</string>
|
<string name="settings_playback_threshold_minutes">Threshold Minutes</string>
|
||||||
<string name="settings_playback_threshold_mode">Threshold Mode</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_minutes_before_end">Minutes before end</string>
|
||||||
<string name="settings_playback_threshold_mode_percentage">Percentage</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">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_threshold_percentage_value">%1$d%</string>
|
||||||
<string name="settings_playback_timeout_instant">Instant</string>
|
<string name="settings_playback_timeout_instant">Instant</string>
|
||||||
<string name="settings_playback_timeout_seconds">%1$ds</string>
|
<string name="settings_playback_timeout_seconds">%1$ds</string>
|
||||||
<string name="settings_playback_timeout_unlimited">Unlimited</string>
|
<string name="settings_playback_timeout_unlimited">Unlimited</string>
|
||||||
<string name="settings_playback_tunneled_playback">Tunneled Playback</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_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_api_key_label">API Key</string>
|
||||||
<string name="settings_tmdb_enable_enrichment">Enable TMDB enrichment</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_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_enter_api_key">Enter your TMDB v3 API key.</string>
|
||||||
<string name="settings_tmdb_language_code_label">Language code</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">Artwork</string>
|
||||||
<string name="settings_tmdb_module_artwork_description">Replace backdrop, poster, and logo with TMDB artwork.</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">Basic Info</string>
|
||||||
<string name="settings_tmdb_module_basic_info_description">Use TMDB title, synopsis, genres, and rating.</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">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">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">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">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_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">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_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">Networks</string>
|
||||||
<string name="settings_tmdb_module_networks_description">Use TMDB network metadata for TV titles.</string>
|
<string name="settings_tmdb_module_networks_description">Networks with logos from TMDB</string>
|
||||||
<string name="settings_tmdb_module_production_companies">Production companies</string>
|
<string name="settings_tmdb_module_production_companies">Productions</string>
|
||||||
<string name="settings_tmdb_module_production_companies_description">Use TMDB production company metadata on the details screen.</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">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_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">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_personal_api_key">Personal API key</string>
|
||||||
<string name="settings_tmdb_preferred_language">Preferred language</string>
|
<string name="settings_tmdb_preferred_language">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_description">TMDB metadata language for title, logo, and enabled fields</string>
|
||||||
<string name="settings_tmdb_section_credentials">CREDENTIALS</string>
|
<string name="settings_tmdb_section_credentials">CREDENTIALS</string>
|
||||||
<string name="settings_tmdb_section_localization">LOCALIZATION</string>
|
<string name="settings_tmdb_section_localization">LOCALIZATION</string>
|
||||||
<string name="settings_tmdb_section_modules">MODULES</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_approval_redirect">After approval, you will be redirected back automatically.</string>
|
||||||
<string name="settings_trakt_authentication">AUTHENTICATION</string>
|
<string name="settings_trakt_authentication">AUTHENTICATION</string>
|
||||||
<string name="settings_trakt_comments">Comments</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_connect">Connect Trakt</string>
|
||||||
<string name="settings_trakt_connected_as">Connected as %1$s</string>
|
<string name="settings_trakt_connected_as">Connected as %1$s</string>
|
||||||
<string name="settings_trakt_default_user">Trakt user</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_failed_open_browser">Failed to open browser</string>
|
||||||
<string name="settings_trakt_features">FEATURES</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_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_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_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>
|
<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_percent">Resume from %1$d%</string>
|
||||||
<string name="streams_resume_from_time">Resume from %1$s</string>
|
<string name="streams_resume_from_time">Resume from %1$s</string>
|
||||||
<string name="streams_size">SIZE %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_close">Close trailer</string>
|
||||||
<string name="trailer_unable_to_play">Unable to play 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>
|
<string name="trakt_lists_load_failed">Failed to load Trakt lists</string>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ data class CollectionSource(
|
||||||
addonId = sourceAddonId,
|
addonId = sourceAddonId,
|
||||||
type = sourceType,
|
type = sourceType,
|
||||||
catalogId = sourceCatalogId,
|
catalogId = sourceCatalogId,
|
||||||
genre = genre,
|
genre = genre.normalizedOptionalGenre(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +193,7 @@ data class CollectionFolder(
|
||||||
addonId = source.addonId,
|
addonId = source.addonId,
|
||||||
type = source.type,
|
type = source.type,
|
||||||
catalogId = source.catalogId,
|
catalogId = source.catalogId,
|
||||||
genre = source.genre,
|
genre = source.genre.normalizedOptionalGenre(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,6 +217,11 @@ data class Collection(
|
||||||
get() = FolderViewMode.fromString(viewMode)
|
get() = FolderViewMode.fromString(viewMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String?.normalizedOptionalGenre(): String? =
|
||||||
|
this
|
||||||
|
?.trim()
|
||||||
|
?.takeIf { it.isNotEmpty() && !it.equals("none", ignoreCase = true) }
|
||||||
|
|
||||||
data class AvailableCatalog(
|
data class AvailableCatalog(
|
||||||
val addonId: String,
|
val addonId: String,
|
||||||
val addonName: String,
|
val addonName: String,
|
||||||
|
|
|
||||||
|
|
@ -358,7 +358,7 @@ private fun HeroContentBlock(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(layout.logoWidthFraction)
|
.fillMaxWidth(layout.logoWidthFraction)
|
||||||
.aspectRatio(2.6f)
|
.aspectRatio(2.6f)
|
||||||
.clickable(enabled = !layout.isTablet && onItemClick != null) {
|
.clickable(enabled = onItemClick != null) {
|
||||||
onItemClick?.invoke(item)
|
onItemClick?.invoke(item)
|
||||||
},
|
},
|
||||||
alignment = if (layout.isTablet) Alignment.CenterStart else Alignment.Center,
|
alignment = if (layout.isTablet) Alignment.CenterStart else Alignment.Center,
|
||||||
|
|
@ -369,7 +369,7 @@ private fun HeroContentBlock(
|
||||||
text = item.name,
|
text = item.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = !layout.isTablet && onItemClick != null) {
|
.clickable(enabled = onItemClick != null) {
|
||||||
onItemClick?.invoke(item)
|
onItemClick?.invoke(item)
|
||||||
},
|
},
|
||||||
style = if (layout.isTablet) {
|
style = if (layout.isTablet) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ data class PlayerAudioLevel(
|
||||||
expect fun LockPlayerToLandscape()
|
expect fun LockPlayerToLandscape()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun EnterImmersivePlayerMode()
|
expect fun EnterImmersivePlayerMode(keepScreenAwake: Boolean)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun ManagePlayerPictureInPicture(
|
expect fun ManagePlayerPictureInPicture(
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,6 @@ fun PlayerScreen(
|
||||||
initialProgressFraction: Float? = null,
|
initialProgressFraction: Float? = null,
|
||||||
) {
|
) {
|
||||||
LockPlayerToLandscape()
|
LockPlayerToLandscape()
|
||||||
EnterImmersivePlayerMode()
|
|
||||||
val playerSettingsUiState by remember {
|
val playerSettingsUiState by remember {
|
||||||
PlayerSettingsRepository.ensureLoaded()
|
PlayerSettingsRepository.ensureLoaded()
|
||||||
PlayerSettingsRepository.uiState
|
PlayerSettingsRepository.uiState
|
||||||
|
|
@ -195,6 +194,9 @@ fun PlayerScreen(
|
||||||
var playerController by remember { mutableStateOf<PlayerEngineController?>(null) }
|
var playerController by remember { mutableStateOf<PlayerEngineController?>(null) }
|
||||||
var playerControllerSourceUrl by remember { mutableStateOf<String?>(null) }
|
var playerControllerSourceUrl by remember { mutableStateOf<String?>(null) }
|
||||||
var errorMessage 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 scrubbingPositionMs by remember { mutableStateOf<Long?>(null) }
|
||||||
var pausedOverlayVisible by remember { mutableStateOf(false) }
|
var pausedOverlayVisible by remember { mutableStateOf(false) }
|
||||||
var gestureFeedback by remember { mutableStateOf<GestureFeedbackState?>(null) }
|
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.Res
|
||||||
import nuvio.composeapp.generated.resources.lang_english
|
import nuvio.composeapp.generated.resources.lang_english
|
||||||
import nuvio.composeapp.generated.resources.lang_french
|
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_spanish
|
||||||
import nuvio.composeapp.generated.resources.lang_portuguese_portugal
|
import nuvio.composeapp.generated.resources.lang_portuguese_portugal
|
||||||
import nuvio.composeapp.generated.resources.lang_turkish
|
import nuvio.composeapp.generated.resources.lang_turkish
|
||||||
|
|
@ -17,6 +18,7 @@ enum class AppLanguage(
|
||||||
) {
|
) {
|
||||||
ENGLISH("en", Res.string.lang_english),
|
ENGLISH("en", Res.string.lang_english),
|
||||||
FRENCH("fr", Res.string.lang_french),
|
FRENCH("fr", Res.string.lang_french),
|
||||||
|
GERMAN("de", Res.string.lang_german),
|
||||||
SPANISH("es", Res.string.lang_spanish),
|
SPANISH("es", Res.string.lang_spanish),
|
||||||
PORTUGUESE("pt", Res.string.lang_portuguese_portugal),
|
PORTUGUESE("pt", Res.string.lang_portuguese_portugal),
|
||||||
TURKISH("tr", Res.string.lang_turkish),
|
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.padding
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -19,7 +16,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.nuvio.app.features.mdblist.MdbListMetadataService
|
import com.nuvio.app.features.mdblist.MdbListMetadataService
|
||||||
import com.nuvio.app.features.mdblist.MdbListSettings
|
import com.nuvio.app.features.mdblist.MdbListSettings
|
||||||
|
|
@ -170,22 +166,13 @@ private fun MdbListApiKeyRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
SettingsSecretTextField(
|
||||||
value = draft,
|
value = draft,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
draft = it
|
draft = it
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
label = stringResource(Res.string.settings_mdb_api_key_label),
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
|
|
||||||
|
|
@ -1960,27 +1960,16 @@ private fun IntroDbApiKeyDialog(
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Surface(
|
SettingsSecretTextField(
|
||||||
shape = RoundedCornerShape(12.dp),
|
value = value,
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
onValueChange = {
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = if (errorMessage != null) 1f else 0.3f)),
|
value = it
|
||||||
) {
|
errorMessage = null
|
||||||
BasicTextField(
|
},
|
||||||
value = value,
|
label = stringResource(Res.string.settings_playback_introdb_api_key),
|
||||||
onValueChange = {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
value = it
|
isError = errorMessage != null,
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (errorMessage != null) {
|
if (errorMessage != null) {
|
||||||
Text(
|
Text(
|
||||||
text = errorMessage!!,
|
text = errorMessage!!,
|
||||||
|
|
@ -2162,4 +2151,3 @@ private fun libassRenderTypeRes(renderType: String): StringResource = when (rend
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun libassRenderTypeLabel(renderType: String): String = stringResource(libassRenderTypeRes(renderType))
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
|
@ -30,6 +31,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -296,118 +298,121 @@ private fun MobileSettingsScreen(
|
||||||
onCheckForUpdatesClick: (() -> Unit)? = null,
|
onCheckForUpdatesClick: (() -> Unit)? = null,
|
||||||
onCollectionsClick: () -> Unit = {},
|
onCollectionsClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
NuvioScreen {
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
stickyHeader {
|
saveableStateHolder.SaveableStateProvider(page.name) {
|
||||||
val previousPage = page.previousPage()
|
NuvioScreen {
|
||||||
NuvioScreenHeader(
|
stickyHeader {
|
||||||
title = stringResource(page.titleRes),
|
val previousPage = page.previousPage()
|
||||||
onBack = previousPage?.let { { onPageChange(it) } },
|
NuvioScreenHeader(
|
||||||
)
|
title = stringResource(page.titleRes),
|
||||||
}
|
onBack = previousPage?.let { { onPageChange(it) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
when (page) {
|
when (page) {
|
||||||
SettingsPage.Root -> settingsRootContent(
|
SettingsPage.Root -> settingsRootContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
onPlaybackClick = { onPageChange(SettingsPage.Playback) },
|
onPlaybackClick = { onPageChange(SettingsPage.Playback) },
|
||||||
onAppearanceClick = { onPageChange(SettingsPage.Appearance) },
|
onAppearanceClick = { onPageChange(SettingsPage.Appearance) },
|
||||||
onNotificationsClick = { onPageChange(SettingsPage.Notifications) },
|
onNotificationsClick = { onPageChange(SettingsPage.Notifications) },
|
||||||
onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
|
onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
|
||||||
onIntegrationsClick = { onPageChange(SettingsPage.Integrations) },
|
onIntegrationsClick = { onPageChange(SettingsPage.Integrations) },
|
||||||
onTraktClick = { onPageChange(SettingsPage.TraktAuthentication) },
|
onTraktClick = { onPageChange(SettingsPage.TraktAuthentication) },
|
||||||
onSupportersContributorsClick = onSupportersContributorsClick,
|
onSupportersContributorsClick = onSupportersContributorsClick,
|
||||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||||
onDownloadsClick = onDownloadsClick,
|
onDownloadsClick = onDownloadsClick,
|
||||||
onAccountClick = onAccountClick,
|
onAccountClick = onAccountClick,
|
||||||
onSwitchProfileClick = onSwitchProfile,
|
onSwitchProfileClick = onSwitchProfile,
|
||||||
)
|
)
|
||||||
SettingsPage.Account -> accountSettingsContent(
|
SettingsPage.Account -> accountSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
)
|
)
|
||||||
SettingsPage.SupportersContributors -> supportersContributorsContent(
|
SettingsPage.SupportersContributors -> supportersContributorsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
)
|
)
|
||||||
SettingsPage.Playback -> playbackSettingsContent(
|
SettingsPage.Playback -> playbackSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
showLoadingOverlay = showLoadingOverlay,
|
showLoadingOverlay = showLoadingOverlay,
|
||||||
holdToSpeedEnabled = holdToSpeedEnabled,
|
holdToSpeedEnabled = holdToSpeedEnabled,
|
||||||
holdToSpeedValue = holdToSpeedValue,
|
holdToSpeedValue = holdToSpeedValue,
|
||||||
preferredAudioLanguage = preferredAudioLanguage,
|
preferredAudioLanguage = preferredAudioLanguage,
|
||||||
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
||||||
preferredSubtitleLanguage = preferredSubtitleLanguage,
|
preferredSubtitleLanguage = preferredSubtitleLanguage,
|
||||||
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
|
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
|
||||||
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
|
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
|
||||||
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
|
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
|
||||||
decoderPriority = decoderPriority,
|
decoderPriority = decoderPriority,
|
||||||
mapDV7ToHevc = mapDV7ToHevc,
|
mapDV7ToHevc = mapDV7ToHevc,
|
||||||
tunnelingEnabled = tunnelingEnabled,
|
tunnelingEnabled = tunnelingEnabled,
|
||||||
useLibass = useLibass,
|
useLibass = useLibass,
|
||||||
libassRenderType = libassRenderType,
|
libassRenderType = libassRenderType,
|
||||||
)
|
)
|
||||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
selectedTheme = selectedTheme,
|
selectedTheme = selectedTheme,
|
||||||
onThemeSelected = onThemeSelected,
|
onThemeSelected = onThemeSelected,
|
||||||
amoledEnabled = amoledEnabled,
|
amoledEnabled = amoledEnabled,
|
||||||
onAmoledToggle = onAmoledToggle,
|
onAmoledToggle = onAmoledToggle,
|
||||||
selectedAppLanguage = selectedAppLanguage,
|
selectedAppLanguage = selectedAppLanguage,
|
||||||
onAppLanguageSelected = onAppLanguageSelected,
|
onAppLanguageSelected = onAppLanguageSelected,
|
||||||
onContinueWatchingClick = onContinueWatchingClick,
|
onContinueWatchingClick = onContinueWatchingClick,
|
||||||
onPosterCustomizationClick = { onPageChange(SettingsPage.PosterCustomization) },
|
onPosterCustomizationClick = { onPageChange(SettingsPage.PosterCustomization) },
|
||||||
)
|
)
|
||||||
SettingsPage.Notifications -> notificationsSettingsContent(
|
SettingsPage.Notifications -> notificationsSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
uiState = episodeReleaseNotificationsUiState,
|
uiState = episodeReleaseNotificationsUiState,
|
||||||
)
|
)
|
||||||
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
isVisible = continueWatchingPreferencesUiState.isVisible,
|
isVisible = continueWatchingPreferencesUiState.isVisible,
|
||||||
style = continueWatchingPreferencesUiState.style,
|
style = continueWatchingPreferencesUiState.style,
|
||||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||||
)
|
)
|
||||||
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
uiState = posterCardStyleUiState,
|
uiState = posterCardStyleUiState,
|
||||||
)
|
)
|
||||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
||||||
onAddonsClick = onAddonsClick,
|
onAddonsClick = onAddonsClick,
|
||||||
onPluginsClick = onPluginsClick,
|
onPluginsClick = onPluginsClick,
|
||||||
onHomescreenClick = onHomescreenClick,
|
onHomescreenClick = onHomescreenClick,
|
||||||
onMetaScreenClick = onMetaScreenClick,
|
onMetaScreenClick = onMetaScreenClick,
|
||||||
onCollectionsClick = onCollectionsClick,
|
onCollectionsClick = onCollectionsClick,
|
||||||
)
|
)
|
||||||
SettingsPage.Addons -> addonsSettingsContent()
|
SettingsPage.Addons -> addonsSettingsContent()
|
||||||
SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
|
SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
|
||||||
SettingsPage.Homescreen -> homescreenSettingsContent(
|
SettingsPage.Homescreen -> homescreenSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
heroEnabled = homescreenHeroEnabled,
|
heroEnabled = homescreenHeroEnabled,
|
||||||
items = homescreenItems,
|
items = homescreenItems,
|
||||||
)
|
)
|
||||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
uiState = metaScreenSettingsUiState,
|
uiState = metaScreenSettingsUiState,
|
||||||
)
|
)
|
||||||
SettingsPage.Integrations -> integrationsContent(
|
SettingsPage.Integrations -> integrationsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
|
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
|
||||||
onMdbListClick = { onPageChange(SettingsPage.MdbListRatings) },
|
onMdbListClick = { onPageChange(SettingsPage.MdbListRatings) },
|
||||||
)
|
)
|
||||||
SettingsPage.TmdbEnrichment -> tmdbSettingsContent(
|
SettingsPage.TmdbEnrichment -> tmdbSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
settings = tmdbSettings,
|
settings = tmdbSettings,
|
||||||
)
|
)
|
||||||
SettingsPage.MdbListRatings -> mdbListSettingsContent(
|
SettingsPage.MdbListRatings -> mdbListSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
settings = mdbListSettings,
|
settings = mdbListSettings,
|
||||||
)
|
)
|
||||||
SettingsPage.TraktAuthentication -> traktSettingsContent(
|
SettingsPage.TraktAuthentication -> traktSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
uiState = traktAuthUiState,
|
uiState = traktAuthUiState,
|
||||||
commentsEnabled = traktCommentsEnabled,
|
commentsEnabled = traktCommentsEnabled,
|
||||||
onCommentsEnabledChange = TraktCommentsSettings::setEnabled,
|
onCommentsEnabledChange = TraktCommentsSettings::setEnabled,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -468,6 +473,8 @@ private fun TabletSettingsScreen(
|
||||||
onPageChange(page)
|
onPageChange(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxSize()) {
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -510,134 +517,138 @@ private fun TabletSettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
saveableStateHolder.SaveableStateProvider(page.name) {
|
||||||
modifier = Modifier.fillMaxSize(),
|
val listState = rememberLazyListState()
|
||||||
contentPadding = PaddingValues(
|
LazyColumn(
|
||||||
start = 40.dp,
|
state = listState,
|
||||||
top = topOffset,
|
modifier = Modifier.fillMaxSize(),
|
||||||
end = 40.dp,
|
contentPadding = PaddingValues(
|
||||||
bottom = 40.dp,
|
start = 40.dp,
|
||||||
),
|
top = topOffset,
|
||||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
end = 40.dp,
|
||||||
) {
|
bottom = 40.dp,
|
||||||
item {
|
),
|
||||||
val previousPage = page.previousPage()
|
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||||
TabletPageHeader(
|
) {
|
||||||
title = if (page == SettingsPage.Root) {
|
item {
|
||||||
stringResource(activeCategory.labelRes)
|
val previousPage = page.previousPage()
|
||||||
} else {
|
TabletPageHeader(
|
||||||
stringResource(page.titleRes)
|
title = if (page == SettingsPage.Root) {
|
||||||
},
|
stringResource(activeCategory.labelRes)
|
||||||
showBack = previousPage != null,
|
} else {
|
||||||
onBack = { previousPage?.let(onPageChange) },
|
stringResource(page.titleRes)
|
||||||
)
|
},
|
||||||
}
|
showBack = previousPage != null,
|
||||||
when (page) {
|
onBack = { previousPage?.let(onPageChange) },
|
||||||
SettingsPage.Root -> settingsRootContent(
|
)
|
||||||
isTablet = true,
|
}
|
||||||
onPlaybackClick = { openInlinePage(SettingsPage.Playback) },
|
when (page) {
|
||||||
onAppearanceClick = { openInlinePage(SettingsPage.Appearance) },
|
SettingsPage.Root -> settingsRootContent(
|
||||||
onNotificationsClick = { openInlinePage(SettingsPage.Notifications) },
|
isTablet = true,
|
||||||
onContentDiscoveryClick = { openInlinePage(SettingsPage.ContentDiscovery) },
|
onPlaybackClick = { openInlinePage(SettingsPage.Playback) },
|
||||||
onIntegrationsClick = { openInlinePage(SettingsPage.Integrations) },
|
onAppearanceClick = { openInlinePage(SettingsPage.Appearance) },
|
||||||
onTraktClick = { openInlinePage(SettingsPage.TraktAuthentication) },
|
onNotificationsClick = { openInlinePage(SettingsPage.Notifications) },
|
||||||
onSupportersContributorsClick = { openInlinePage(SettingsPage.SupportersContributors) },
|
onContentDiscoveryClick = { openInlinePage(SettingsPage.ContentDiscovery) },
|
||||||
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
onIntegrationsClick = { openInlinePage(SettingsPage.Integrations) },
|
||||||
onDownloadsClick = onDownloadsClick,
|
onTraktClick = { openInlinePage(SettingsPage.TraktAuthentication) },
|
||||||
onAccountClick = { openInlinePage(SettingsPage.Account) },
|
onSupportersContributorsClick = { openInlinePage(SettingsPage.SupportersContributors) },
|
||||||
onSwitchProfileClick = onSwitchProfile,
|
onCheckForUpdatesClick = onCheckForUpdatesClick,
|
||||||
showAccountSection = activeCategory == SettingsCategory.Account,
|
onDownloadsClick = onDownloadsClick,
|
||||||
showGeneralSection = activeCategory == SettingsCategory.General,
|
onAccountClick = { openInlinePage(SettingsPage.Account) },
|
||||||
showAboutSection = activeCategory == SettingsCategory.About,
|
onSwitchProfileClick = onSwitchProfile,
|
||||||
)
|
showAccountSection = activeCategory == SettingsCategory.Account,
|
||||||
SettingsPage.Account -> accountSettingsContent(
|
showGeneralSection = activeCategory == SettingsCategory.General,
|
||||||
isTablet = true,
|
showAboutSection = activeCategory == SettingsCategory.About,
|
||||||
)
|
)
|
||||||
SettingsPage.SupportersContributors -> supportersContributorsContent(
|
SettingsPage.Account -> accountSettingsContent(
|
||||||
isTablet = true,
|
isTablet = true,
|
||||||
)
|
)
|
||||||
SettingsPage.Playback -> playbackSettingsContent(
|
SettingsPage.SupportersContributors -> supportersContributorsContent(
|
||||||
isTablet = true,
|
isTablet = true,
|
||||||
showLoadingOverlay = showLoadingOverlay,
|
)
|
||||||
holdToSpeedEnabled = holdToSpeedEnabled,
|
SettingsPage.Playback -> playbackSettingsContent(
|
||||||
holdToSpeedValue = holdToSpeedValue,
|
isTablet = true,
|
||||||
preferredAudioLanguage = preferredAudioLanguage,
|
showLoadingOverlay = showLoadingOverlay,
|
||||||
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
holdToSpeedEnabled = holdToSpeedEnabled,
|
||||||
preferredSubtitleLanguage = preferredSubtitleLanguage,
|
holdToSpeedValue = holdToSpeedValue,
|
||||||
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
|
preferredAudioLanguage = preferredAudioLanguage,
|
||||||
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
|
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
||||||
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
|
preferredSubtitleLanguage = preferredSubtitleLanguage,
|
||||||
decoderPriority = decoderPriority,
|
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
|
||||||
mapDV7ToHevc = mapDV7ToHevc,
|
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
|
||||||
tunnelingEnabled = tunnelingEnabled,
|
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
|
||||||
useLibass = useLibass,
|
decoderPriority = decoderPriority,
|
||||||
libassRenderType = libassRenderType,
|
mapDV7ToHevc = mapDV7ToHevc,
|
||||||
)
|
tunnelingEnabled = tunnelingEnabled,
|
||||||
SettingsPage.Appearance -> appearanceSettingsContent(
|
useLibass = useLibass,
|
||||||
isTablet = true,
|
libassRenderType = libassRenderType,
|
||||||
selectedTheme = selectedTheme,
|
)
|
||||||
onThemeSelected = onThemeSelected,
|
SettingsPage.Appearance -> appearanceSettingsContent(
|
||||||
amoledEnabled = amoledEnabled,
|
isTablet = true,
|
||||||
onAmoledToggle = onAmoledToggle,
|
selectedTheme = selectedTheme,
|
||||||
selectedAppLanguage = selectedAppLanguage,
|
onThemeSelected = onThemeSelected,
|
||||||
onAppLanguageSelected = onAppLanguageSelected,
|
amoledEnabled = amoledEnabled,
|
||||||
onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) },
|
onAmoledToggle = onAmoledToggle,
|
||||||
onPosterCustomizationClick = { openInlinePage(SettingsPage.PosterCustomization) },
|
selectedAppLanguage = selectedAppLanguage,
|
||||||
)
|
onAppLanguageSelected = onAppLanguageSelected,
|
||||||
SettingsPage.Notifications -> notificationsSettingsContent(
|
onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) },
|
||||||
isTablet = true,
|
onPosterCustomizationClick = { openInlinePage(SettingsPage.PosterCustomization) },
|
||||||
uiState = episodeReleaseNotificationsUiState,
|
)
|
||||||
)
|
SettingsPage.Notifications -> notificationsSettingsContent(
|
||||||
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
isTablet = true,
|
||||||
isTablet = true,
|
uiState = episodeReleaseNotificationsUiState,
|
||||||
isVisible = continueWatchingPreferencesUiState.isVisible,
|
)
|
||||||
style = continueWatchingPreferencesUiState.style,
|
SettingsPage.ContinueWatching -> continueWatchingSettingsContent(
|
||||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
isTablet = true,
|
||||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
isVisible = continueWatchingPreferencesUiState.isVisible,
|
||||||
)
|
style = continueWatchingPreferencesUiState.style,
|
||||||
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||||
isTablet = true,
|
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||||
uiState = posterCardStyleUiState,
|
)
|
||||||
)
|
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
||||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
isTablet = true,
|
||||||
isTablet = true,
|
uiState = posterCardStyleUiState,
|
||||||
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
)
|
||||||
onAddonsClick = { openInlinePage(SettingsPage.Addons) },
|
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||||
onPluginsClick = { openInlinePage(SettingsPage.Plugins) },
|
isTablet = true,
|
||||||
onHomescreenClick = { openInlinePage(SettingsPage.Homescreen) },
|
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
||||||
onMetaScreenClick = { openInlinePage(SettingsPage.MetaScreen) },
|
onAddonsClick = { openInlinePage(SettingsPage.Addons) },
|
||||||
onCollectionsClick = onCollectionsClick,
|
onPluginsClick = { openInlinePage(SettingsPage.Plugins) },
|
||||||
)
|
onHomescreenClick = { openInlinePage(SettingsPage.Homescreen) },
|
||||||
SettingsPage.Addons -> addonsSettingsContent()
|
onMetaScreenClick = { openInlinePage(SettingsPage.MetaScreen) },
|
||||||
SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
|
onCollectionsClick = onCollectionsClick,
|
||||||
SettingsPage.Homescreen -> homescreenSettingsContent(
|
)
|
||||||
isTablet = true,
|
SettingsPage.Addons -> addonsSettingsContent()
|
||||||
heroEnabled = homescreenHeroEnabled,
|
SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
|
||||||
items = homescreenItems,
|
SettingsPage.Homescreen -> homescreenSettingsContent(
|
||||||
)
|
isTablet = true,
|
||||||
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
heroEnabled = homescreenHeroEnabled,
|
||||||
isTablet = true,
|
items = homescreenItems,
|
||||||
uiState = metaScreenSettingsUiState,
|
)
|
||||||
)
|
SettingsPage.MetaScreen -> metaScreenSettingsContent(
|
||||||
SettingsPage.Integrations -> integrationsContent(
|
isTablet = true,
|
||||||
isTablet = true,
|
uiState = metaScreenSettingsUiState,
|
||||||
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
|
)
|
||||||
onMdbListClick = { onPageChange(SettingsPage.MdbListRatings) },
|
SettingsPage.Integrations -> integrationsContent(
|
||||||
)
|
isTablet = true,
|
||||||
SettingsPage.TmdbEnrichment -> tmdbSettingsContent(
|
onTmdbClick = { onPageChange(SettingsPage.TmdbEnrichment) },
|
||||||
isTablet = true,
|
onMdbListClick = { onPageChange(SettingsPage.MdbListRatings) },
|
||||||
settings = tmdbSettings,
|
)
|
||||||
)
|
SettingsPage.TmdbEnrichment -> tmdbSettingsContent(
|
||||||
SettingsPage.MdbListRatings -> mdbListSettingsContent(
|
isTablet = true,
|
||||||
isTablet = true,
|
settings = tmdbSettings,
|
||||||
settings = mdbListSettings,
|
)
|
||||||
)
|
SettingsPage.MdbListRatings -> mdbListSettingsContent(
|
||||||
SettingsPage.TraktAuthentication -> traktSettingsContent(
|
isTablet = true,
|
||||||
isTablet = true,
|
settings = mdbListSettings,
|
||||||
uiState = traktAuthUiState,
|
)
|
||||||
commentsEnabled = traktCommentsEnabled,
|
SettingsPage.TraktAuthentication -> traktSettingsContent(
|
||||||
onCommentsEnabledChange = TraktCommentsSettings::setEnabled,
|
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()
|
val normalizedDraft = draft.trim()
|
||||||
|
|
||||||
OutlinedTextField(
|
SettingsSecretTextField(
|
||||||
value = draft,
|
value = draft,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
draft = it
|
draft = it
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
label = stringResource(Res.string.settings_tmdb_api_key_label),
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,18 @@ data class StreamItem(
|
||||||
val directPlaybackUrl: String?
|
val directPlaybackUrl: String?
|
||||||
get() = url ?: externalUrl
|
get() = url ?: externalUrl
|
||||||
|
|
||||||
|
val isTorrentStream: Boolean
|
||||||
|
get() = !infoHash.isNullOrBlank() ||
|
||||||
|
url.isMagnetLink() ||
|
||||||
|
externalUrl.isMagnetLink()
|
||||||
|
|
||||||
val hasPlayableSource: Boolean
|
val hasPlayableSource: Boolean
|
||||||
get() = url != null || infoHash != null || externalUrl != null
|
get() = url != null || infoHash != null || externalUrl != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String?.isMagnetLink(): Boolean =
|
||||||
|
this?.trimStart()?.startsWith("magnet:", ignoreCase = true) == true
|
||||||
|
|
||||||
data class StreamBehaviorHints(
|
data class StreamBehaviorHints(
|
||||||
val bingeGroup: String? = null,
|
val bingeGroup: String? = null,
|
||||||
val notWebReady: Boolean = false,
|
val notWebReady: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ fun StreamsScreen(
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val streamLinkCopiedText = stringResource(Res.string.streams_link_copied)
|
val streamLinkCopiedText = stringResource(Res.string.streams_link_copied)
|
||||||
val noDirectStreamLinkText = stringResource(Res.string.streams_no_direct_link)
|
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 streamActionsTarget by remember(videoId) { mutableStateOf<StreamItem?>(null) }
|
||||||
var preferredFilterApplied by remember(videoId) { mutableStateOf(false) }
|
var preferredFilterApplied by remember(videoId) { mutableStateOf(false) }
|
||||||
val storedProgress = if (startFromBeginning) {
|
val storedProgress = if (startFromBeginning) {
|
||||||
|
|
@ -205,7 +206,13 @@ fun StreamsScreen(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
resumePositionMs = effectiveResumePositionMs,
|
resumePositionMs = effectiveResumePositionMs,
|
||||||
resumeProgressFraction = effectiveResumeProgressFraction,
|
resumeProgressFraction = effectiveResumeProgressFraction,
|
||||||
onStreamSelected = onStreamSelected,
|
onStreamSelected = { stream, positionMs, progressFraction ->
|
||||||
|
if (stream.isTorrentStream) {
|
||||||
|
NuvioToastController.show(torrentUnsupportedText)
|
||||||
|
} else {
|
||||||
|
onStreamSelected(stream, positionMs, progressFraction)
|
||||||
|
}
|
||||||
|
},
|
||||||
onStreamLongPress = { stream -> streamActionsTarget = stream },
|
onStreamLongPress = { stream -> streamActionsTarget = stream },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -220,7 +227,13 @@ fun StreamsScreen(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
resumePositionMs = effectiveResumePositionMs,
|
resumePositionMs = effectiveResumePositionMs,
|
||||||
resumeProgressFraction = effectiveResumeProgressFraction,
|
resumeProgressFraction = effectiveResumeProgressFraction,
|
||||||
onStreamSelected = onStreamSelected,
|
onStreamSelected = { stream, positionMs, progressFraction ->
|
||||||
|
if (stream.isTorrentStream) {
|
||||||
|
NuvioToastController.show(torrentUnsupportedText)
|
||||||
|
} else {
|
||||||
|
onStreamSelected(stream, positionMs, progressFraction)
|
||||||
|
}
|
||||||
|
},
|
||||||
onStreamLongPress = { stream -> streamActionsTarget = stream },
|
onStreamLongPress = { stream -> streamActionsTarget = stream },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -830,7 +843,7 @@ private fun LazyListScope.streamSection(
|
||||||
StreamCard(
|
StreamCard(
|
||||||
stream = stream,
|
stream = stream,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (stream.directPlaybackUrl != null) {
|
if (stream.directPlaybackUrl != null || stream.isTorrentStream) {
|
||||||
onStreamSelected(stream, resumePositionMs, resumeProgressFraction)
|
onStreamSelected(stream, resumePositionMs, resumeProgressFraction)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -936,7 +949,7 @@ private fun StreamCard(
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val isEnabled = stream.directPlaybackUrl != null
|
val isEnabled = stream.directPlaybackUrl != null || stream.isTorrentStream
|
||||||
val cardShape = RoundedCornerShape(12.dp)
|
val cardShape = RoundedCornerShape(12.dp)
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
package com.nuvio.app.features.watching.domain
|
package com.nuvio.app.features.watching.domain
|
||||||
|
|
||||||
private const val InProgressStartThresholdFraction = 0.02f
|
private const val CompletionThresholdFraction = 0.90
|
||||||
private const val CompletionThresholdFraction = 0.85
|
private const val ProgressStoreThresholdMs = 1_000L
|
||||||
private const val InProgressStartThresholdMinMs = 30_000L
|
|
||||||
private const val UpcomingNextSeasonWindowDays = 7
|
private const val UpcomingNextSeasonWindowDays = 7
|
||||||
|
|
||||||
fun watchedKey(
|
fun watchedKey(
|
||||||
|
|
@ -14,17 +13,7 @@ fun watchedKey(
|
||||||
fun shouldStoreProgress(
|
fun shouldStoreProgress(
|
||||||
positionMs: Long,
|
positionMs: Long,
|
||||||
durationMs: Long,
|
durationMs: Long,
|
||||||
): Boolean {
|
): Boolean = positionMs >= ProgressStoreThresholdMs
|
||||||
val thresholdMs = if (durationMs > 0L) {
|
|
||||||
maxOf(
|
|
||||||
InProgressStartThresholdMinMs,
|
|
||||||
(durationMs * InProgressStartThresholdFraction).toLong(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
1L
|
|
||||||
}
|
|
||||||
return positionMs >= thresholdMs
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isProgressComplete(
|
fun isProgressComplete(
|
||||||
positionMs: Long,
|
positionMs: Long,
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,17 @@ class WatchProgressRulesTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `save threshold uses max of thirty seconds and two percent`() {
|
fun `save threshold starts after one second`() {
|
||||||
assertFalse(shouldStoreWatchProgress(positionMs = 29_999L, durationMs = 600_000L))
|
assertFalse(shouldStoreWatchProgress(positionMs = 999L, durationMs = 600_000L))
|
||||||
assertTrue(shouldStoreWatchProgress(positionMs = 30_000L, durationMs = 600_000L))
|
assertTrue(shouldStoreWatchProgress(positionMs = 1_000L, durationMs = 600_000L))
|
||||||
assertFalse(shouldStoreWatchProgress(positionMs = 119_999L, durationMs = 6_000_000L))
|
assertTrue(shouldStoreWatchProgress(positionMs = 1_000L, durationMs = 0L))
|
||||||
assertTrue(shouldStoreWatchProgress(positionMs = 120_000L, durationMs = 6_000_000L))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `completion detects watched threshold remaining time and ended state`() {
|
fun `completion detects watched threshold remaining time and ended state`() {
|
||||||
assertTrue(isWatchProgressComplete(positionMs = 920_000L, durationMs = 1_000_000L, isEnded = false))
|
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))
|
assertTrue(isWatchProgressComplete(positionMs = 1L, durationMs = 0L, isEnded = true))
|
||||||
assertFalse(isWatchProgressComplete(positionMs = 200_000L, durationMs = 1_000_000L, isEnded = false))
|
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.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import platform.Foundation.NSNotificationCenter
|
import platform.Foundation.NSNotificationCenter
|
||||||
|
|
@ -32,10 +33,12 @@ actual fun LockPlayerToLandscape() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun EnterImmersivePlayerMode() {
|
actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) {
|
||||||
|
SideEffect {
|
||||||
|
UIApplication.sharedApplication.setIdleTimerDisabled(keepScreenAwake)
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
// Request idle timer disabled to keep screen awake during playback
|
|
||||||
UIApplication.sharedApplication.setIdleTimerDisabled(true)
|
|
||||||
onDispose {
|
onDispose {
|
||||||
UIApplication.sharedApplication.setIdleTimerDisabled(false)
|
UIApplication.sharedApplication.setIdleTimerDisabled(false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -579,15 +579,29 @@ final class MPVPlayerViewController: UIViewController {
|
||||||
for i in 0..<count {
|
for i in 0..<count {
|
||||||
let type = getString("track-list/\(i)/type") ?? ""
|
let type = getString("track-list/\(i)/type") ?? ""
|
||||||
let id = getInt("track-list/\(i)/id")
|
let id = getInt("track-list/\(i)/id")
|
||||||
let title = getString("track-list/\(i)/title") ?? ""
|
let title = getTrackString(i, "title")
|
||||||
let lang = getString("track-list/\(i)/lang") ?? ""
|
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 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" {
|
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
|
audioIdx += 1
|
||||||
} else if type == "sub" {
|
} 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
|
subIdx += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -595,6 +609,98 @@ final class MPVPlayerViewController: UIViewController {
|
||||||
subtitleTracks = subs
|
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() {
|
private func clearPlaybackError() {
|
||||||
errorStateLock.lock()
|
errorStateLock.lock()
|
||||||
recentPlaybackLogs.removeAll(keepingCapacity: true)
|
recentPlaybackLogs.removeAll(keepingCapacity: true)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue