feat(video): add custom zoom controls

close #1159
This commit is contained in:
edde746
2026-05-29 00:06:47 +02:00
parent 47276e1bac
commit 2b7ea5fe02
52 changed files with 1123 additions and 106 deletions
@@ -122,6 +122,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
private var surfaceContainer: FrameLayout? = null private var surfaceContainer: FrameLayout? = null
private var videoAspectContainer: AspectRatioFrameLayout? = null private var videoAspectContainer: AspectRatioFrameLayout? = null
private var subtitleView: SubtitleView? = null private var subtitleView: SubtitleView? = null
private var videoZoomScale: Float = 1.0f
private var assHandler: AssHandler? = null private var assHandler: AssHandler? = null
private var overlayLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null private var overlayLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
private var lastVideoSize: VideoSize? = null private var lastVideoSize: VideoSize? = null
@@ -1193,18 +1194,26 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
val containerHeight = contentView.height val containerHeight = contentView.height
if (containerWidth == 0 || containerHeight == 0) return if (containerWidth == 0 || containerHeight == 0) return
// In cover/stretch modes subtitles stay at container size so they never get // In cover/stretch/zoomed-in modes subtitles stay at container size so they
// cropped or distorted. In letterbox mode they follow the video rect so they // never get cropped. In letterbox mode they follow the visible video rect.
// anchor to the bottom of the video (matching MPV's default sub positioning).
val isLetterbox = videoAspectContainer?.resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FIT val isLetterbox = videoAspectContainer?.resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FIT
val (subWidth, subHeight) = if (isLetterbox) { val (subWidth, subHeight) = if (isLetterbox) {
val videoAspect = (videoWidth * pixelRatio) / videoHeight val videoAspect = (videoWidth * pixelRatio) / videoHeight
val containerAspect = containerWidth.toFloat() / containerHeight val containerAspect = containerWidth.toFloat() / containerHeight
if (videoAspect > containerAspect) { val (baseWidth, baseHeight) = if (videoAspect > containerAspect) {
containerWidth to (containerWidth / videoAspect).toInt() containerWidth to (containerWidth / videoAspect).toInt()
} else { } else {
(containerHeight * videoAspect).toInt() to containerHeight (containerHeight * videoAspect).toInt() to containerHeight
} }
if (videoZoomScale < 0.999f) {
val zoomedWidth = ((baseWidth * videoZoomScale).toInt()).coerceAtLeast(1)
val zoomedHeight = ((baseHeight * videoZoomScale).toInt()).coerceAtLeast(1)
zoomedWidth to zoomedHeight
} else if (videoZoomScale > 1.001f) {
containerWidth to containerHeight
} else {
baseWidth to baseHeight
}
} else { } else {
containerWidth to containerHeight containerWidth to containerHeight
} }
@@ -1235,6 +1244,21 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
} }
} }
fun setVideoZoom(scale: Double) {
if (disposing) return
val clamped = scale.coerceIn(0.5, 2.0).toFloat()
activity.runOnUiThread {
videoZoomScale = clamped
videoAspectContainer?.scaleX = clamped
videoAspectContainer?.scaleY = clamped
lastVideoSize?.let { vs ->
if (vs.width > 0 && vs.height > 0) {
updateSubtitleViewSize(vs.width, vs.height, vs.pixelWidthHeightRatio)
}
}
}
}
private fun emitTrackList() { private fun emitTrackList() {
val player = exoPlayer ?: return val player = exoPlayer ?: return
val tracks = player.currentTracks val tracks = player.currentTracks
@@ -148,6 +148,7 @@ class ExoPlayerPlugin :
} }
"setSubtitleStyle" -> handleSetSubtitleStyle(call, result) "setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
"setBoxFitMode" -> handleSetBoxFitMode(call, result) "setBoxFitMode" -> handleSetBoxFitMode(call, result)
"setVideoZoom" -> handleSetVideoZoom(call, result)
"setDvConversionMode" -> handleSetDvConversionMode(call, result) "setDvConversionMode" -> handleSetDvConversionMode(call, result)
"observeProperty" -> handleObserveProperty(call, result) "observeProperty" -> handleObserveProperty(call, result)
"setMpvProperty" -> handleSetMpvProperty(call, result) "setMpvProperty" -> handleSetMpvProperty(call, result)
@@ -550,6 +551,22 @@ class ExoPlayerPlugin :
} ?: result.success(null) } ?: result.success(null)
} }
private fun handleSetVideoZoom(call: MethodCall, result: MethodChannel.Result) {
val scale = call.argument<Number>("scale")?.toDouble()
if (scale == null) {
result.error("INVALID_ARGS", "Missing 'scale'", null)
return
}
if (usingMpvFallback) {
result.success(null)
return
}
activity?.runOnUiThread {
playerCore?.setVideoZoom(scale)
result.success(null)
} ?: result.success(null)
}
private fun handleSetDvConversionMode(call: MethodCall, result: MethodChannel.Result) { private fun handleSetDvConversionMode(call: MethodCall, result: MethodChannel.Result) {
val mode = call.argument<String>("mode") val mode = call.argument<String>("mode")
if (mode == null) { if (mode == null) {
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Увеличи скоростта", "speedIncrease": "Увеличи скоростта",
"speedDecrease": "Намали скоростта", "speedDecrease": "Намали скоростта",
"speedReset": "Нулирай скоростта", "speedReset": "Нулирай скоростта",
"zoomIn": "Увеличи мащаба",
"zoomOut": "Намали мащаба",
"zoomReset": "Нулирай мащаба",
"subSeekNext": "Отиди до следващ субтитър", "subSeekNext": "Отиди до следващ субтитър",
"subSeekPrev": "Отиди до предишен субтитър", "subSeekPrev": "Отиди до предишен субтитър",
"shaderToggle": "Превключи шейдъри", "shaderToggle": "Превключи шейдъри",
@@ -444,6 +447,7 @@
"pipActive": "Възпроизвеждане в режим картина в картината", "pipActive": "Възпроизвеждане в режим картина в картината",
"pipFailed": "Режимът картина в картината не успя да стартира", "pipFailed": "Режимът картина в картината не успя да стартира",
"screenshotSaved": "Екранната снимка е запазена", "screenshotSaved": "Екранната снимка е запазена",
"zoomPercent": "Мащаб ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Изисква Android 8.0 или по-нова версия", "androidVersion": "Изисква Android 8.0 или по-нова версия",
"iosVersion": "Изисква iOS 15.0 или по-нова версия", "iosVersion": "Изисква iOS 15.0 или по-нова версия",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Скорост на възпроизвеждане", "playbackSpeed": "Скорост на възпроизвеждане",
"zoom": "Мащаб",
"sleepTimer": "Таймер за заспиване", "sleepTimer": "Таймер за заспиване",
"audioSync": "Синхронизация на аудио", "audioSync": "Синхронизация на аудио",
"subtitleSync": "Синхронизация на субтитри", "subtitleSync": "Синхронизация на субтитри",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Øg hastighed", "speedIncrease": "Øg hastighed",
"speedDecrease": "Sænk hastighed", "speedDecrease": "Sænk hastighed",
"speedReset": "Nulstil hastighed", "speedReset": "Nulstil hastighed",
"zoomIn": "Zoom ind",
"zoomOut": "Zoom ud",
"zoomReset": "Nulstil zoom",
"subSeekNext": "Søg til næste undertekst", "subSeekNext": "Søg til næste undertekst",
"subSeekPrev": "Søg til forrige undertekst", "subSeekPrev": "Søg til forrige undertekst",
"shaderToggle": "Skift shadere", "shaderToggle": "Skift shadere",
@@ -444,6 +447,7 @@
"pipActive": "Afspiller i billede-i-billede", "pipActive": "Afspiller i billede-i-billede",
"pipFailed": "Billede-i-billede kunne ikke starte", "pipFailed": "Billede-i-billede kunne ikke starte",
"screenshotSaved": "Skærmbillede gemt", "screenshotSaved": "Skærmbillede gemt",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Kræver Android 8.0 eller nyere", "androidVersion": "Kræver Android 8.0 eller nyere",
"iosVersion": "Kræver iOS 15.0 eller nyere", "iosVersion": "Kræver iOS 15.0 eller nyere",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Afspilningshastighed", "playbackSpeed": "Afspilningshastighed",
"zoom": "Zoom",
"sleepTimer": "Sove-timer", "sleepTimer": "Sove-timer",
"audioSync": "Lydsynkronisering", "audioSync": "Lydsynkronisering",
"subtitleSync": "Undertekstsynkronisering", "subtitleSync": "Undertekstsynkronisering",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Geschwindigkeit erhöhen", "speedIncrease": "Geschwindigkeit erhöhen",
"speedDecrease": "Geschwindigkeit verringern", "speedDecrease": "Geschwindigkeit verringern",
"speedReset": "Geschwindigkeit zurücksetzen", "speedReset": "Geschwindigkeit zurücksetzen",
"zoomIn": "Vergrößern",
"zoomOut": "Verkleinern",
"zoomReset": "Zoom zurücksetzen",
"subSeekNext": "Zum nächsten Untertitel springen", "subSeekNext": "Zum nächsten Untertitel springen",
"subSeekPrev": "Zum vorherigen Untertitel springen", "subSeekPrev": "Zum vorherigen Untertitel springen",
"shaderToggle": "Shader umschalten", "shaderToggle": "Shader umschalten",
@@ -444,6 +447,7 @@
"pipActive": "Wiedergabe im Bild-in-Bild-Modus", "pipActive": "Wiedergabe im Bild-in-Bild-Modus",
"pipFailed": "Bild-in-Bild konnte nicht gestartet werden", "pipFailed": "Bild-in-Bild konnte nicht gestartet werden",
"screenshotSaved": "Screenshot gespeichert", "screenshotSaved": "Screenshot gespeichert",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Erfordert Android 8.0 oder neuer", "androidVersion": "Erfordert Android 8.0 oder neuer",
"iosVersion": "Erfordert iOS 15.0 oder neuer", "iosVersion": "Erfordert iOS 15.0 oder neuer",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Wiedergabegeschwindigkeit", "playbackSpeed": "Wiedergabegeschwindigkeit",
"zoom": "Zoom",
"sleepTimer": "Schlaftimer", "sleepTimer": "Schlaftimer",
"audioSync": "Audio-Synchronisation", "audioSync": "Audio-Synchronisation",
"subtitleSync": "Untertitel-Synchronisation", "subtitleSync": "Untertitel-Synchronisation",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Increase Speed", "speedIncrease": "Increase Speed",
"speedDecrease": "Decrease Speed", "speedDecrease": "Decrease Speed",
"speedReset": "Reset Speed", "speedReset": "Reset Speed",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"zoomReset": "Reset Zoom",
"subSeekNext": "Seek to Next Subtitle", "subSeekNext": "Seek to Next Subtitle",
"subSeekPrev": "Seek to Previous Subtitle", "subSeekPrev": "Seek to Previous Subtitle",
"shaderToggle": "Toggle Shaders", "shaderToggle": "Toggle Shaders",
@@ -444,6 +447,7 @@
"pipActive": "Playing in Picture-in-Picture", "pipActive": "Playing in Picture-in-Picture",
"pipFailed": "Picture-in-picture failed to start", "pipFailed": "Picture-in-picture failed to start",
"screenshotSaved": "Screenshot saved", "screenshotSaved": "Screenshot saved",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Requires Android 8.0 or newer", "androidVersion": "Requires Android 8.0 or newer",
"iosVersion": "Requires iOS 15.0 or newer", "iosVersion": "Requires iOS 15.0 or newer",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Playback Speed", "playbackSpeed": "Playback Speed",
"zoom": "Zoom",
"sleepTimer": "Sleep Timer", "sleepTimer": "Sleep Timer",
"audioSync": "Audio Sync", "audioSync": "Audio Sync",
"subtitleSync": "Subtitle Sync", "subtitleSync": "Subtitle Sync",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Aumentar Velocidad", "speedIncrease": "Aumentar Velocidad",
"speedDecrease": "Disminuir Velocidad", "speedDecrease": "Disminuir Velocidad",
"speedReset": "Restablecer Velocidad", "speedReset": "Restablecer Velocidad",
"zoomIn": "Acercar",
"zoomOut": "Alejar",
"zoomReset": "Restablecer zoom",
"subSeekNext": "Ir al Siguiente Subtítulo", "subSeekNext": "Ir al Siguiente Subtítulo",
"subSeekPrev": "Ir al Anterior Subtítulo", "subSeekPrev": "Ir al Anterior Subtítulo",
"shaderToggle": "Alternar Shaders", "shaderToggle": "Alternar Shaders",
@@ -444,6 +447,7 @@
"pipActive": "Reproduciendo en Imagen en Imagen", "pipActive": "Reproduciendo en Imagen en Imagen",
"pipFailed": "Error al iniciar Imagen en Imagen", "pipFailed": "Error al iniciar Imagen en Imagen",
"screenshotSaved": "Captura de pantalla guardada", "screenshotSaved": "Captura de pantalla guardada",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Requiere Android 8.0 o más reciente", "androidVersion": "Requiere Android 8.0 o más reciente",
"iosVersion": "Requiere iOS 15.0 o más reciente", "iosVersion": "Requiere iOS 15.0 o más reciente",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Velocidad de reproducción", "playbackSpeed": "Velocidad de reproducción",
"zoom": "Zoom",
"sleepTimer": "Temporizador de apagado", "sleepTimer": "Temporizador de apagado",
"audioSync": "Sincronización de audio", "audioSync": "Sincronización de audio",
"subtitleSync": "Sincronización de subtítulos", "subtitleSync": "Sincronización de subtítulos",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Augmenter la vitesse", "speedIncrease": "Augmenter la vitesse",
"speedDecrease": "Réduire la vitesse", "speedDecrease": "Réduire la vitesse",
"speedReset": "Réinitialiser la vitesse", "speedReset": "Réinitialiser la vitesse",
"zoomIn": "Zoom avant",
"zoomOut": "Zoom arrière",
"zoomReset": "Réinitialiser le zoom",
"subSeekNext": "Rechercher le sous-titre suivant", "subSeekNext": "Rechercher le sous-titre suivant",
"subSeekPrev": "Rechercher le sous-titre précédent", "subSeekPrev": "Rechercher le sous-titre précédent",
"shaderToggle": "Activer/désactiver les shaders", "shaderToggle": "Activer/désactiver les shaders",
@@ -444,6 +447,7 @@
"pipActive": "Lecture en mode image dans l'image", "pipActive": "Lecture en mode image dans l'image",
"pipFailed": "Échec du démarrage du mode image dans l'image", "pipFailed": "Échec du démarrage du mode image dans l'image",
"screenshotSaved": "Capture d'écran enregistrée", "screenshotSaved": "Capture d'écran enregistrée",
"zoomPercent": "Zoom ${percent} %",
"pipErrors": { "pipErrors": {
"androidVersion": "Nécessite Android 8.0 ou plus récent", "androidVersion": "Nécessite Android 8.0 ou plus récent",
"iosVersion": "Nécessite iOS 15.0 ou plus récent", "iosVersion": "Nécessite iOS 15.0 ou plus récent",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Vitesse de lecture", "playbackSpeed": "Vitesse de lecture",
"zoom": "Zoom",
"sleepTimer": "Minuterie de mise en veille", "sleepTimer": "Minuterie de mise en veille",
"audioSync": "Synchronisation audio", "audioSync": "Synchronisation audio",
"subtitleSync": "Synchronisation des sous-titres", "subtitleSync": "Synchronisation des sous-titres",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Aumenta velocità", "speedIncrease": "Aumenta velocità",
"speedDecrease": "Diminuisci velocità", "speedDecrease": "Diminuisci velocità",
"speedReset": "Ripristina velocità", "speedReset": "Ripristina velocità",
"zoomIn": "Aumenta zoom",
"zoomOut": "Riduci zoom",
"zoomReset": "Ripristina zoom",
"subSeekNext": "Vai al sottotitolo successivo", "subSeekNext": "Vai al sottotitolo successivo",
"subSeekPrev": "Vai al sottotitolo precedente", "subSeekPrev": "Vai al sottotitolo precedente",
"shaderToggle": "Attiva/disattiva shader", "shaderToggle": "Attiva/disattiva shader",
@@ -444,6 +447,7 @@
"pipActive": "Riproduzione in Picture-in-Picture", "pipActive": "Riproduzione in Picture-in-Picture",
"pipFailed": "Impossibile avviare la modalità Picture-in-Picture", "pipFailed": "Impossibile avviare la modalità Picture-in-Picture",
"screenshotSaved": "Schermata salvata", "screenshotSaved": "Schermata salvata",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Richiede Android 8.0 o versioni successive", "androidVersion": "Richiede Android 8.0 o versioni successive",
"iosVersion": "Richiede iOS 15.0 o versioni successive", "iosVersion": "Richiede iOS 15.0 o versioni successive",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Velocità di riproduzione", "playbackSpeed": "Velocità di riproduzione",
"zoom": "Zoom",
"sleepTimer": "Timer di spegnimento", "sleepTimer": "Timer di spegnimento",
"audioSync": "Sincronizzazione audio", "audioSync": "Sincronizzazione audio",
"subtitleSync": "Sincronizzazione sottotitoli", "subtitleSync": "Sincronizzazione sottotitoli",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "速度を上げる", "speedIncrease": "速度を上げる",
"speedDecrease": "速度を下げる", "speedDecrease": "速度を下げる",
"speedReset": "速度をリセット", "speedReset": "速度をリセット",
"zoomIn": "ズームイン",
"zoomOut": "ズームアウト",
"zoomReset": "ズームをリセット",
"subSeekNext": "次の字幕にシーク", "subSeekNext": "次の字幕にシーク",
"subSeekPrev": "前の字幕にシーク", "subSeekPrev": "前の字幕にシーク",
"shaderToggle": "シェーダー切替", "shaderToggle": "シェーダー切替",
@@ -444,6 +447,7 @@
"pipActive": "ピクチャーインピクチャーで再生中", "pipActive": "ピクチャーインピクチャーで再生中",
"pipFailed": "ピクチャーインピクチャーの開始に失敗しました", "pipFailed": "ピクチャーインピクチャーの開始に失敗しました",
"screenshotSaved": "スクリーンショットを保存しました", "screenshotSaved": "スクリーンショットを保存しました",
"zoomPercent": "ズーム ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Android 8.0以降が必要です", "androidVersion": "Android 8.0以降が必要です",
"iosVersion": "iOS 15.0以降が必要です", "iosVersion": "iOS 15.0以降が必要です",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "再生速度", "playbackSpeed": "再生速度",
"zoom": "ズーム",
"sleepTimer": "スリープタイマー", "sleepTimer": "スリープタイマー",
"audioSync": "音声同期", "audioSync": "音声同期",
"subtitleSync": "字幕同期", "subtitleSync": "字幕同期",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "속도 높이기", "speedIncrease": "속도 높이기",
"speedDecrease": "속도 낮추기", "speedDecrease": "속도 낮추기",
"speedReset": "속도 초기화", "speedReset": "속도 초기화",
"zoomIn": "확대",
"zoomOut": "축소",
"zoomReset": "확대/축소 초기화",
"subSeekNext": "다음 자막으로 이동", "subSeekNext": "다음 자막으로 이동",
"subSeekPrev": "이전 자막으로 이동", "subSeekPrev": "이전 자막으로 이동",
"shaderToggle": "셰이더 전환", "shaderToggle": "셰이더 전환",
@@ -444,6 +447,7 @@
"pipActive": "화면 속 화면으로 재생 중", "pipActive": "화면 속 화면으로 재생 중",
"pipFailed": "화면 속 화면 모드를 시작할 수 없습니다", "pipFailed": "화면 속 화면 모드를 시작할 수 없습니다",
"screenshotSaved": "스크린샷 저장됨", "screenshotSaved": "스크린샷 저장됨",
"zoomPercent": "확대/축소 ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Android 8.0 이상이 필요합니다", "androidVersion": "Android 8.0 이상이 필요합니다",
"iosVersion": "iOS 15.0 이상이 필요합니다", "iosVersion": "iOS 15.0 이상이 필요합니다",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "재생 속도", "playbackSpeed": "재생 속도",
"zoom": "확대/축소",
"sleepTimer": "취침 타이머", "sleepTimer": "취침 타이머",
"audioSync": "오디오 동기화", "audioSync": "오디오 동기화",
"subtitleSync": "자막 동기화", "subtitleSync": "자막 동기화",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Øk hastighet", "speedIncrease": "Øk hastighet",
"speedDecrease": "Reduser hastighet", "speedDecrease": "Reduser hastighet",
"speedReset": "Tilbakestill hastighet", "speedReset": "Tilbakestill hastighet",
"zoomIn": "Zoom inn",
"zoomOut": "Zoom ut",
"zoomReset": "Tilbakestill zoom",
"subSeekNext": "Spol til neste undertekst", "subSeekNext": "Spol til neste undertekst",
"subSeekPrev": "Spol til forrige undertekst", "subSeekPrev": "Spol til forrige undertekst",
"shaderToggle": "Veksle shadere", "shaderToggle": "Veksle shadere",
@@ -444,6 +447,7 @@
"pipActive": "Spiller i bilde-i-bilde", "pipActive": "Spiller i bilde-i-bilde",
"pipFailed": "Bilde-i-bilde kunne ikke starte", "pipFailed": "Bilde-i-bilde kunne ikke starte",
"screenshotSaved": "Skjermbilde lagret", "screenshotSaved": "Skjermbilde lagret",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Krever Android 8.0 eller nyere", "androidVersion": "Krever Android 8.0 eller nyere",
"iosVersion": "Krever iOS 15.0 eller nyere", "iosVersion": "Krever iOS 15.0 eller nyere",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Avspillingshastighet", "playbackSpeed": "Avspillingshastighet",
"zoom": "Zoom",
"sleepTimer": "Søvntimer", "sleepTimer": "Søvntimer",
"audioSync": "Lydsynkronisering", "audioSync": "Lydsynkronisering",
"subtitleSync": "Undertekstsynkronisering", "subtitleSync": "Undertekstsynkronisering",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Snelheid verhogen", "speedIncrease": "Snelheid verhogen",
"speedDecrease": "Snelheid verlagen", "speedDecrease": "Snelheid verlagen",
"speedReset": "Snelheid resetten", "speedReset": "Snelheid resetten",
"zoomIn": "Inzoomen",
"zoomOut": "Uitzoomen",
"zoomReset": "Zoom resetten",
"subSeekNext": "Naar volgende ondertitel", "subSeekNext": "Naar volgende ondertitel",
"subSeekPrev": "Naar vorige ondertitel", "subSeekPrev": "Naar vorige ondertitel",
"shaderToggle": "Shaders aan/uit", "shaderToggle": "Shaders aan/uit",
@@ -444,6 +447,7 @@
"pipActive": "Afspelen in beeld-in-beeld", "pipActive": "Afspelen in beeld-in-beeld",
"pipFailed": "Beeld-in-beeld kon niet worden gestart", "pipFailed": "Beeld-in-beeld kon niet worden gestart",
"screenshotSaved": "Schermafbeelding opgeslagen", "screenshotSaved": "Schermafbeelding opgeslagen",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Vereist Android 8.0 of nieuwer", "androidVersion": "Vereist Android 8.0 of nieuwer",
"iosVersion": "Vereist iOS 15.0 of nieuwer", "iosVersion": "Vereist iOS 15.0 of nieuwer",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Afspeelsnelheid", "playbackSpeed": "Afspeelsnelheid",
"zoom": "Zoom",
"sleepTimer": "Slaaptimer", "sleepTimer": "Slaaptimer",
"audioSync": "Audio synchronisatie", "audioSync": "Audio synchronisatie",
"subtitleSync": "Ondertitel synchronisatie", "subtitleSync": "Ondertitel synchronisatie",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Zwiększ prędkość", "speedIncrease": "Zwiększ prędkość",
"speedDecrease": "Zmniejsz prędkość", "speedDecrease": "Zmniejsz prędkość",
"speedReset": "Zresetuj prędkość", "speedReset": "Zresetuj prędkość",
"zoomIn": "Powiększ",
"zoomOut": "Pomniejsz",
"zoomReset": "Zresetuj zoom",
"subSeekNext": "Przewiń do następnego napisu", "subSeekNext": "Przewiń do następnego napisu",
"subSeekPrev": "Przewiń do poprzedniego napisu", "subSeekPrev": "Przewiń do poprzedniego napisu",
"shaderToggle": "Przełącz shadery", "shaderToggle": "Przełącz shadery",
@@ -444,6 +447,7 @@
"pipActive": "Odtwarzanie w trybie obraz w obrazie", "pipActive": "Odtwarzanie w trybie obraz w obrazie",
"pipFailed": "Nie udało się uruchomić trybu obraz w obrazie", "pipFailed": "Nie udało się uruchomić trybu obraz w obrazie",
"screenshotSaved": "Zrzut ekranu zapisany", "screenshotSaved": "Zrzut ekranu zapisany",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Wymaga Androida 8.0 lub nowszego", "androidVersion": "Wymaga Androida 8.0 lub nowszego",
"iosVersion": "Wymaga iOS 15.0 lub nowszego", "iosVersion": "Wymaga iOS 15.0 lub nowszego",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Prędkość odtwarzania", "playbackSpeed": "Prędkość odtwarzania",
"zoom": "Zoom",
"sleepTimer": "Wyłącznik czasowy", "sleepTimer": "Wyłącznik czasowy",
"audioSync": "Synchronizacja audio", "audioSync": "Synchronizacja audio",
"subtitleSync": "Synchronizacja napisów", "subtitleSync": "Synchronizacja napisów",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Aumentar Velocidade", "speedIncrease": "Aumentar Velocidade",
"speedDecrease": "Diminuir Velocidade", "speedDecrease": "Diminuir Velocidade",
"speedReset": "Redefinir Velocidade", "speedReset": "Redefinir Velocidade",
"zoomIn": "Aumentar zoom",
"zoomOut": "Diminuir zoom",
"zoomReset": "Redefinir zoom",
"subSeekNext": "Ir para Próxima Legenda", "subSeekNext": "Ir para Próxima Legenda",
"subSeekPrev": "Ir para Legenda Anterior", "subSeekPrev": "Ir para Legenda Anterior",
"shaderToggle": "Alternar Shaders", "shaderToggle": "Alternar Shaders",
@@ -444,6 +447,7 @@
"pipActive": "Reproduzindo em Picture-in-Picture", "pipActive": "Reproduzindo em Picture-in-Picture",
"pipFailed": "Falha ao iniciar picture-in-picture", "pipFailed": "Falha ao iniciar picture-in-picture",
"screenshotSaved": "Captura de tela salva", "screenshotSaved": "Captura de tela salva",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Requer Android 8.0 ou superior", "androidVersion": "Requer Android 8.0 ou superior",
"iosVersion": "Requer iOS 15.0 ou superior", "iosVersion": "Requer iOS 15.0 ou superior",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Velocidade de Reprodução", "playbackSpeed": "Velocidade de Reprodução",
"zoom": "Zoom",
"sleepTimer": "Timer de Sono", "sleepTimer": "Timer de Sono",
"audioSync": "Sincronia de Áudio", "audioSync": "Sincronia de Áudio",
"subtitleSync": "Sincronia de Legendas", "subtitleSync": "Sincronia de Legendas",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Увеличить скорость", "speedIncrease": "Увеличить скорость",
"speedDecrease": "Уменьшить скорость", "speedDecrease": "Уменьшить скорость",
"speedReset": "Сбросить скорость", "speedReset": "Сбросить скорость",
"zoomIn": "Увеличить масштаб",
"zoomOut": "Уменьшить масштаб",
"zoomReset": "Сбросить масштаб",
"subSeekNext": "К следующему субтитру", "subSeekNext": "К следующему субтитру",
"subSeekPrev": "К предыдущему субтитру", "subSeekPrev": "К предыдущему субтитру",
"shaderToggle": "Вкл./выкл. шейдеры", "shaderToggle": "Вкл./выкл. шейдеры",
@@ -444,6 +447,7 @@
"pipActive": "Воспроизводится в режиме «картинка в картинке»", "pipActive": "Воспроизводится в режиме «картинка в картинке»",
"pipFailed": "Не удалось запустить режим «картинка в картинке»", "pipFailed": "Не удалось запустить режим «картинка в картинке»",
"screenshotSaved": "Снимок экрана сохранён", "screenshotSaved": "Снимок экрана сохранён",
"zoomPercent": "Масштаб ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Требуется Android 8.0 или новее", "androidVersion": "Требуется Android 8.0 или новее",
"iosVersion": "Требуется iOS 15.0 или новее", "iosVersion": "Требуется iOS 15.0 или новее",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Скорость воспроизведения", "playbackSpeed": "Скорость воспроизведения",
"zoom": "Масштаб",
"sleepTimer": "Таймер сна", "sleepTimer": "Таймер сна",
"audioSync": "Синхронизация аудио", "audioSync": "Синхронизация аудио",
"subtitleSync": "Синхронизация субтитров", "subtitleSync": "Синхронизация субтитров",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 16 /// Locales: 16
/// Strings: 18864 (1179 per locale) /// Strings: 18944 (1184 per locale)
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsBg extends TranslationsVideoControlsEn {
@override String get pipActive => 'Възпроизвеждане в режим картина в картината'; @override String get pipActive => 'Възпроизвеждане в режим картина в картината';
@override String get pipFailed => 'Режимът картина в картината не успя да стартира'; @override String get pipFailed => 'Режимът картина в картината не успя да стартира';
@override String get screenshotSaved => 'Екранната снимка е запазена'; @override String get screenshotSaved => 'Екранната снимка е запазена';
@override String zoomPercent({required Object percent}) => 'Мащаб ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsBg pipErrors = _TranslationsVideoControlsPipErrorsBg._(_root); @override late final _TranslationsVideoControlsPipErrorsBg pipErrors = _TranslationsVideoControlsPipErrorsBg._(_root);
@override String get chapters => 'Глави'; @override String get chapters => 'Глави';
@override String get noChaptersAvailable => 'Няма налични глави'; @override String get noChaptersAvailable => 'Няма налични глави';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsBg extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Скорост на възпроизвеждане'; @override String get playbackSpeed => 'Скорост на възпроизвеждане';
@override String get zoom => 'Мащаб';
@override String get sleepTimer => 'Таймер за заспиване'; @override String get sleepTimer => 'Таймер за заспиване';
@override String get audioSync => 'Синхронизация на аудио'; @override String get audioSync => 'Синхронизация на аудио';
@override String get subtitleSync => 'Синхронизация на субтитри'; @override String get subtitleSync => 'Синхронизация на субтитри';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsBg extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Увеличи скоростта'; @override String get speedIncrease => 'Увеличи скоростта';
@override String get speedDecrease => 'Намали скоростта'; @override String get speedDecrease => 'Намали скоростта';
@override String get speedReset => 'Нулирай скоростта'; @override String get speedReset => 'Нулирай скоростта';
@override String get zoomIn => 'Увеличи мащаба';
@override String get zoomOut => 'Намали мащаба';
@override String get zoomReset => 'Нулирай мащаба';
@override String get subSeekNext => 'Отиди до следващ субтитър'; @override String get subSeekNext => 'Отиди до следващ субтитър';
@override String get subSeekPrev => 'Отиди до предишен субтитър'; @override String get subSeekPrev => 'Отиди до предишен субтитър';
@override String get shaderToggle => 'Превключи шейдъри'; @override String get shaderToggle => 'Превключи шейдъри';
@@ -2099,6 +2104,9 @@ extension on TranslationsBg {
'hotkeys.actions.speedIncrease' => 'Увеличи скоростта', 'hotkeys.actions.speedIncrease' => 'Увеличи скоростта',
'hotkeys.actions.speedDecrease' => 'Намали скоростта', 'hotkeys.actions.speedDecrease' => 'Намали скоростта',
'hotkeys.actions.speedReset' => 'Нулирай скоростта', 'hotkeys.actions.speedReset' => 'Нулирай скоростта',
'hotkeys.actions.zoomIn' => 'Увеличи мащаба',
'hotkeys.actions.zoomOut' => 'Намали мащаба',
'hotkeys.actions.zoomReset' => 'Нулирай мащаба',
'hotkeys.actions.subSeekNext' => 'Отиди до следващ субтитър', 'hotkeys.actions.subSeekNext' => 'Отиди до следващ субтитър',
'hotkeys.actions.subSeekPrev' => 'Отиди до предишен субтитър', 'hotkeys.actions.subSeekPrev' => 'Отиди до предишен субтитър',
'hotkeys.actions.shaderToggle' => 'Превключи шейдъри', 'hotkeys.actions.shaderToggle' => 'Превключи шейдъри',
@@ -2224,6 +2232,7 @@ extension on TranslationsBg {
'videoControls.pipActive' => 'Възпроизвеждане в режим картина в картината', 'videoControls.pipActive' => 'Възпроизвеждане в режим картина в картината',
'videoControls.pipFailed' => 'Режимът картина в картината не успя да стартира', 'videoControls.pipFailed' => 'Режимът картина в картината не успя да стартира',
'videoControls.screenshotSaved' => 'Екранната снимка е запазена', 'videoControls.screenshotSaved' => 'Екранната снимка е запазена',
'videoControls.zoomPercent' => ({required Object percent}) => 'Мащаб ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Изисква Android 8.0 или по-нова версия', 'videoControls.pipErrors.androidVersion' => 'Изисква Android 8.0 или по-нова версия',
'videoControls.pipErrors.iosVersion' => 'Изисква iOS 15.0 или по-нова версия', 'videoControls.pipErrors.iosVersion' => 'Изисква iOS 15.0 или по-нова версия',
'videoControls.pipErrors.permissionDisabled' => 'Режимът картина в картината е изключен. Включете го в системните настройки.', 'videoControls.pipErrors.permissionDisabled' => 'Режимът картина в картината е изключен. Включете го в системните настройки.',
@@ -2316,12 +2325,12 @@ extension on TranslationsBg {
'profiles.addPlezyProfile' => 'Добави Plezy профил', 'profiles.addPlezyProfile' => 'Добави Plezy профил',
'profiles.switchingProfile' => 'Смяна на профил…', 'profiles.switchingProfile' => 'Смяна на профил…',
'profiles.deleteThisProfileTitle' => 'Да се изтрие ли този профил?', 'profiles.deleteThisProfileTitle' => 'Да се изтрие ли този профил?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Премахване на ${displayName}. Връзките не се засягат.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Премахване на ${displayName}. Връзките не се засягат.',
'profiles.active' => 'Активен', 'profiles.active' => 'Активен',
'profiles.manage' => 'Управление', 'profiles.manage' => 'Управление',
'profiles.delete' => 'Изтрий', 'profiles.delete' => 'Изтрий',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Изход', 'profiles.signOut' => 'Изход',
'profiles.signOutPlexTitle' => 'Изход от Plex?', 'profiles.signOutPlexTitle' => 'Изход от Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Премахване на ${displayName} и всички Plex Home потребители? Можете да влезете отново по всяко време.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Премахване на ${displayName} и всички Plex Home потребители? Можете да влезете отново по всяко време.',
@@ -2801,6 +2810,7 @@ extension on TranslationsBg {
'companionRemote.remote.audio' => 'Аудио', 'companionRemote.remote.audio' => 'Аудио',
'companionRemote.remote.searchHint' => 'Търсене на настолен компютър...', 'companionRemote.remote.searchHint' => 'Търсене на настолен компютър...',
'videoSettings.playbackSpeed' => 'Скорост на възпроизвеждане', 'videoSettings.playbackSpeed' => 'Скорост на възпроизвеждане',
'videoSettings.zoom' => 'Мащаб',
'videoSettings.sleepTimer' => 'Таймер за заспиване', 'videoSettings.sleepTimer' => 'Таймер за заспиване',
'videoSettings.audioSync' => 'Синхронизация на аудио', 'videoSettings.audioSync' => 'Синхронизация на аудио',
'videoSettings.subtitleSync' => 'Синхронизация на субтитри', 'videoSettings.subtitleSync' => 'Синхронизация на субтитри',
@@ -2829,13 +2839,13 @@ extension on TranslationsBg {
'metadataEdit.basicInfo' => 'Основна информация', 'metadataEdit.basicInfo' => 'Основна информация',
'metadataEdit.artwork' => 'Обложка', 'metadataEdit.artwork' => 'Обложка',
'metadataEdit.advancedSettings' => 'Разширени настройки', 'metadataEdit.advancedSettings' => 'Разширени настройки',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Заглавие', 'metadataEdit.title' => 'Заглавие',
'metadataEdit.sortTitle' => 'Заглавие за сортиране', 'metadataEdit.sortTitle' => 'Заглавие за сортиране',
'metadataEdit.originalTitle' => 'Оригинално заглавие', 'metadataEdit.originalTitle' => 'Оригинално заглавие',
'metadataEdit.releaseDate' => 'Дата на излизане', 'metadataEdit.releaseDate' => 'Дата на излизане',
'metadataEdit.contentRating' => 'Възрастов рейтинг', 'metadataEdit.contentRating' => 'Възрастов рейтинг',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Студио', 'metadataEdit.studio' => 'Студио',
'metadataEdit.tagline' => 'Слоган', 'metadataEdit.tagline' => 'Слоган',
'metadataEdit.summary' => 'Резюме', 'metadataEdit.summary' => 'Резюме',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsDa extends TranslationsVideoControlsEn {
@override String get pipActive => 'Afspiller i billede-i-billede'; @override String get pipActive => 'Afspiller i billede-i-billede';
@override String get pipFailed => 'Billede-i-billede kunne ikke starte'; @override String get pipFailed => 'Billede-i-billede kunne ikke starte';
@override String get screenshotSaved => 'Skærmbillede gemt'; @override String get screenshotSaved => 'Skærmbillede gemt';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsDa pipErrors = _TranslationsVideoControlsPipErrorsDa._(_root); @override late final _TranslationsVideoControlsPipErrorsDa pipErrors = _TranslationsVideoControlsPipErrorsDa._(_root);
@override String get chapters => 'Kapitler'; @override String get chapters => 'Kapitler';
@override String get noChaptersAvailable => 'Ingen kapitler tilgængelige'; @override String get noChaptersAvailable => 'Ingen kapitler tilgængelige';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsDa extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Afspilningshastighed'; @override String get playbackSpeed => 'Afspilningshastighed';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Sove-timer'; @override String get sleepTimer => 'Sove-timer';
@override String get audioSync => 'Lydsynkronisering'; @override String get audioSync => 'Lydsynkronisering';
@override String get subtitleSync => 'Undertekstsynkronisering'; @override String get subtitleSync => 'Undertekstsynkronisering';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsDa extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Øg hastighed'; @override String get speedIncrease => 'Øg hastighed';
@override String get speedDecrease => 'Sænk hastighed'; @override String get speedDecrease => 'Sænk hastighed';
@override String get speedReset => 'Nulstil hastighed'; @override String get speedReset => 'Nulstil hastighed';
@override String get zoomIn => 'Zoom ind';
@override String get zoomOut => 'Zoom ud';
@override String get zoomReset => 'Nulstil zoom';
@override String get subSeekNext => 'Søg til næste undertekst'; @override String get subSeekNext => 'Søg til næste undertekst';
@override String get subSeekPrev => 'Søg til forrige undertekst'; @override String get subSeekPrev => 'Søg til forrige undertekst';
@override String get shaderToggle => 'Skift shadere'; @override String get shaderToggle => 'Skift shadere';
@@ -2099,6 +2104,9 @@ extension on TranslationsDa {
'hotkeys.actions.speedIncrease' => 'Øg hastighed', 'hotkeys.actions.speedIncrease' => 'Øg hastighed',
'hotkeys.actions.speedDecrease' => 'Sænk hastighed', 'hotkeys.actions.speedDecrease' => 'Sænk hastighed',
'hotkeys.actions.speedReset' => 'Nulstil hastighed', 'hotkeys.actions.speedReset' => 'Nulstil hastighed',
'hotkeys.actions.zoomIn' => 'Zoom ind',
'hotkeys.actions.zoomOut' => 'Zoom ud',
'hotkeys.actions.zoomReset' => 'Nulstil zoom',
'hotkeys.actions.subSeekNext' => 'Søg til næste undertekst', 'hotkeys.actions.subSeekNext' => 'Søg til næste undertekst',
'hotkeys.actions.subSeekPrev' => 'Søg til forrige undertekst', 'hotkeys.actions.subSeekPrev' => 'Søg til forrige undertekst',
'hotkeys.actions.shaderToggle' => 'Skift shadere', 'hotkeys.actions.shaderToggle' => 'Skift shadere',
@@ -2224,6 +2232,7 @@ extension on TranslationsDa {
'videoControls.pipActive' => 'Afspiller i billede-i-billede', 'videoControls.pipActive' => 'Afspiller i billede-i-billede',
'videoControls.pipFailed' => 'Billede-i-billede kunne ikke starte', 'videoControls.pipFailed' => 'Billede-i-billede kunne ikke starte',
'videoControls.screenshotSaved' => 'Skærmbillede gemt', 'videoControls.screenshotSaved' => 'Skærmbillede gemt',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Kræver Android 8.0 eller nyere', 'videoControls.pipErrors.androidVersion' => 'Kræver Android 8.0 eller nyere',
'videoControls.pipErrors.iosVersion' => 'Kræver iOS 15.0 eller nyere', 'videoControls.pipErrors.iosVersion' => 'Kræver iOS 15.0 eller nyere',
'videoControls.pipErrors.permissionDisabled' => 'Billede-i-billede er deaktiveret. Slå det til i systemindstillinger.', 'videoControls.pipErrors.permissionDisabled' => 'Billede-i-billede er deaktiveret. Slå det til i systemindstillinger.',
@@ -2316,12 +2325,12 @@ extension on TranslationsDa {
'profiles.addPlezyProfile' => 'Tilføj Plezy-profil', 'profiles.addPlezyProfile' => 'Tilføj Plezy-profil',
'profiles.switchingProfile' => 'Skifter profil…', 'profiles.switchingProfile' => 'Skifter profil…',
'profiles.deleteThisProfileTitle' => 'Slet denne profil?', 'profiles.deleteThisProfileTitle' => 'Slet denne profil?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Fjern ${displayName}. Forbindelser påvirkes ikke.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Fjern ${displayName}. Forbindelser påvirkes ikke.',
'profiles.active' => 'Aktiv', 'profiles.active' => 'Aktiv',
'profiles.manage' => 'Administrer', 'profiles.manage' => 'Administrer',
'profiles.delete' => 'Slet', 'profiles.delete' => 'Slet',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Log ud', 'profiles.signOut' => 'Log ud',
'profiles.signOutPlexTitle' => 'Log ud af Plex?', 'profiles.signOutPlexTitle' => 'Log ud af Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Fjern ${displayName} og alle Plex Home-brugere? Log ind igen når som helst.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Fjern ${displayName} og alle Plex Home-brugere? Log ind igen når som helst.',
@@ -2801,6 +2810,7 @@ extension on TranslationsDa {
'companionRemote.remote.audio' => 'Lyd', 'companionRemote.remote.audio' => 'Lyd',
'companionRemote.remote.searchHint' => 'Søg på desktop...', 'companionRemote.remote.searchHint' => 'Søg på desktop...',
'videoSettings.playbackSpeed' => 'Afspilningshastighed', 'videoSettings.playbackSpeed' => 'Afspilningshastighed',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Sove-timer', 'videoSettings.sleepTimer' => 'Sove-timer',
'videoSettings.audioSync' => 'Lydsynkronisering', 'videoSettings.audioSync' => 'Lydsynkronisering',
'videoSettings.subtitleSync' => 'Undertekstsynkronisering', 'videoSettings.subtitleSync' => 'Undertekstsynkronisering',
@@ -2829,13 +2839,13 @@ extension on TranslationsDa {
'metadataEdit.basicInfo' => 'Grundlæggende info', 'metadataEdit.basicInfo' => 'Grundlæggende info',
'metadataEdit.artwork' => 'Grafik', 'metadataEdit.artwork' => 'Grafik',
'metadataEdit.advancedSettings' => 'Avancerede indstillinger', 'metadataEdit.advancedSettings' => 'Avancerede indstillinger',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Titel', 'metadataEdit.title' => 'Titel',
'metadataEdit.sortTitle' => 'Sorteringstitel', 'metadataEdit.sortTitle' => 'Sorteringstitel',
'metadataEdit.originalTitle' => 'Originaltitel', 'metadataEdit.originalTitle' => 'Originaltitel',
'metadataEdit.releaseDate' => 'Udgivelsesdato', 'metadataEdit.releaseDate' => 'Udgivelsesdato',
'metadataEdit.contentRating' => 'Aldersgrænse', 'metadataEdit.contentRating' => 'Aldersgrænse',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studie', 'metadataEdit.studio' => 'Studie',
'metadataEdit.tagline' => 'Tagline', 'metadataEdit.tagline' => 'Tagline',
'metadataEdit.summary' => 'Resumé', 'metadataEdit.summary' => 'Resumé',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsDe extends TranslationsVideoControlsEn {
@override String get pipActive => 'Wiedergabe im Bild-in-Bild-Modus'; @override String get pipActive => 'Wiedergabe im Bild-in-Bild-Modus';
@override String get pipFailed => 'Bild-in-Bild konnte nicht gestartet werden'; @override String get pipFailed => 'Bild-in-Bild konnte nicht gestartet werden';
@override String get screenshotSaved => 'Screenshot gespeichert'; @override String get screenshotSaved => 'Screenshot gespeichert';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsDe pipErrors = _TranslationsVideoControlsPipErrorsDe._(_root); @override late final _TranslationsVideoControlsPipErrorsDe pipErrors = _TranslationsVideoControlsPipErrorsDe._(_root);
@override String get chapters => 'Kapitel'; @override String get chapters => 'Kapitel';
@override String get noChaptersAvailable => 'Keine Kapitel verfügbar'; @override String get noChaptersAvailable => 'Keine Kapitel verfügbar';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsDe extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Wiedergabegeschwindigkeit'; @override String get playbackSpeed => 'Wiedergabegeschwindigkeit';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Schlaftimer'; @override String get sleepTimer => 'Schlaftimer';
@override String get audioSync => 'Audio-Synchronisation'; @override String get audioSync => 'Audio-Synchronisation';
@override String get subtitleSync => 'Untertitel-Synchronisation'; @override String get subtitleSync => 'Untertitel-Synchronisation';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsDe extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Geschwindigkeit erhöhen'; @override String get speedIncrease => 'Geschwindigkeit erhöhen';
@override String get speedDecrease => 'Geschwindigkeit verringern'; @override String get speedDecrease => 'Geschwindigkeit verringern';
@override String get speedReset => 'Geschwindigkeit zurücksetzen'; @override String get speedReset => 'Geschwindigkeit zurücksetzen';
@override String get zoomIn => 'Vergrößern';
@override String get zoomOut => 'Verkleinern';
@override String get zoomReset => 'Zoom zurücksetzen';
@override String get subSeekNext => 'Zum nächsten Untertitel springen'; @override String get subSeekNext => 'Zum nächsten Untertitel springen';
@override String get subSeekPrev => 'Zum vorherigen Untertitel springen'; @override String get subSeekPrev => 'Zum vorherigen Untertitel springen';
@override String get shaderToggle => 'Shader umschalten'; @override String get shaderToggle => 'Shader umschalten';
@@ -2099,6 +2104,9 @@ extension on TranslationsDe {
'hotkeys.actions.speedIncrease' => 'Geschwindigkeit erhöhen', 'hotkeys.actions.speedIncrease' => 'Geschwindigkeit erhöhen',
'hotkeys.actions.speedDecrease' => 'Geschwindigkeit verringern', 'hotkeys.actions.speedDecrease' => 'Geschwindigkeit verringern',
'hotkeys.actions.speedReset' => 'Geschwindigkeit zurücksetzen', 'hotkeys.actions.speedReset' => 'Geschwindigkeit zurücksetzen',
'hotkeys.actions.zoomIn' => 'Vergrößern',
'hotkeys.actions.zoomOut' => 'Verkleinern',
'hotkeys.actions.zoomReset' => 'Zoom zurücksetzen',
'hotkeys.actions.subSeekNext' => 'Zum nächsten Untertitel springen', 'hotkeys.actions.subSeekNext' => 'Zum nächsten Untertitel springen',
'hotkeys.actions.subSeekPrev' => 'Zum vorherigen Untertitel springen', 'hotkeys.actions.subSeekPrev' => 'Zum vorherigen Untertitel springen',
'hotkeys.actions.shaderToggle' => 'Shader umschalten', 'hotkeys.actions.shaderToggle' => 'Shader umschalten',
@@ -2224,6 +2232,7 @@ extension on TranslationsDe {
'videoControls.pipActive' => 'Wiedergabe im Bild-in-Bild-Modus', 'videoControls.pipActive' => 'Wiedergabe im Bild-in-Bild-Modus',
'videoControls.pipFailed' => 'Bild-in-Bild konnte nicht gestartet werden', 'videoControls.pipFailed' => 'Bild-in-Bild konnte nicht gestartet werden',
'videoControls.screenshotSaved' => 'Screenshot gespeichert', 'videoControls.screenshotSaved' => 'Screenshot gespeichert',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Erfordert Android 8.0 oder neuer', 'videoControls.pipErrors.androidVersion' => 'Erfordert Android 8.0 oder neuer',
'videoControls.pipErrors.iosVersion' => 'Erfordert iOS 15.0 oder neuer', 'videoControls.pipErrors.iosVersion' => 'Erfordert iOS 15.0 oder neuer',
'videoControls.pipErrors.permissionDisabled' => 'Bild-in-Bild ist deaktiviert. Aktiviere es in den Systemeinstellungen.', 'videoControls.pipErrors.permissionDisabled' => 'Bild-in-Bild ist deaktiviert. Aktiviere es in den Systemeinstellungen.',
@@ -2316,12 +2325,12 @@ extension on TranslationsDe {
'profiles.addPlezyProfile' => 'Plezy-Profil hinzufügen', 'profiles.addPlezyProfile' => 'Plezy-Profil hinzufügen',
'profiles.switchingProfile' => 'Profil wird gewechselt…', 'profiles.switchingProfile' => 'Profil wird gewechselt…',
'profiles.deleteThisProfileTitle' => 'Dieses Profil löschen?', 'profiles.deleteThisProfileTitle' => 'Dieses Profil löschen?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName} entfernen. Verbindungen bleiben unberührt.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName} entfernen. Verbindungen bleiben unberührt.',
'profiles.active' => 'Aktiv', 'profiles.active' => 'Aktiv',
'profiles.manage' => 'Verwalten', 'profiles.manage' => 'Verwalten',
'profiles.delete' => 'Löschen', 'profiles.delete' => 'Löschen',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Abmelden', 'profiles.signOut' => 'Abmelden',
'profiles.signOutPlexTitle' => 'Von Plex abmelden?', 'profiles.signOutPlexTitle' => 'Von Plex abmelden?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} und alle Plex Home-Benutzer entfernen? Du kannst dich jederzeit wieder anmelden.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} und alle Plex Home-Benutzer entfernen? Du kannst dich jederzeit wieder anmelden.',
@@ -2801,6 +2810,7 @@ extension on TranslationsDe {
'companionRemote.remote.audio' => 'Audio', 'companionRemote.remote.audio' => 'Audio',
'companionRemote.remote.searchHint' => 'Auf Desktop suchen...', 'companionRemote.remote.searchHint' => 'Auf Desktop suchen...',
'videoSettings.playbackSpeed' => 'Wiedergabegeschwindigkeit', 'videoSettings.playbackSpeed' => 'Wiedergabegeschwindigkeit',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Schlaftimer', 'videoSettings.sleepTimer' => 'Schlaftimer',
'videoSettings.audioSync' => 'Audio-Synchronisation', 'videoSettings.audioSync' => 'Audio-Synchronisation',
'videoSettings.subtitleSync' => 'Untertitel-Synchronisation', 'videoSettings.subtitleSync' => 'Untertitel-Synchronisation',
@@ -2829,13 +2839,13 @@ extension on TranslationsDe {
'metadataEdit.basicInfo' => 'Grundlegende Infos', 'metadataEdit.basicInfo' => 'Grundlegende Infos',
'metadataEdit.artwork' => 'Artwork', 'metadataEdit.artwork' => 'Artwork',
'metadataEdit.advancedSettings' => 'Erweiterte Einstellungen', 'metadataEdit.advancedSettings' => 'Erweiterte Einstellungen',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Titel', 'metadataEdit.title' => 'Titel',
'metadataEdit.sortTitle' => 'Sortiertitel', 'metadataEdit.sortTitle' => 'Sortiertitel',
'metadataEdit.originalTitle' => 'Originaltitel', 'metadataEdit.originalTitle' => 'Originaltitel',
'metadataEdit.releaseDate' => 'Erscheinungsdatum', 'metadataEdit.releaseDate' => 'Erscheinungsdatum',
'metadataEdit.contentRating' => 'Altersfreigabe', 'metadataEdit.contentRating' => 'Altersfreigabe',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studio', 'metadataEdit.studio' => 'Studio',
'metadataEdit.tagline' => 'Tagline', 'metadataEdit.tagline' => 'Tagline',
'metadataEdit.summary' => 'Zusammenfassung', 'metadataEdit.summary' => 'Zusammenfassung',
+24 -4
View File
@@ -1396,6 +1396,9 @@ class TranslationsVideoControlsEn {
/// en: 'Screenshot saved' /// en: 'Screenshot saved'
String get screenshotSaved => 'Screenshot saved'; String get screenshotSaved => 'Screenshot saved';
/// en: 'Zoom ${percent}%'
String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
late final TranslationsVideoControlsPipErrorsEn pipErrors = TranslationsVideoControlsPipErrorsEn.internal(_root); late final TranslationsVideoControlsPipErrorsEn pipErrors = TranslationsVideoControlsPipErrorsEn.internal(_root);
/// en: 'Chapters' /// en: 'Chapters'
@@ -3060,6 +3063,9 @@ class TranslationsVideoSettingsEn {
/// en: 'Playback Speed' /// en: 'Playback Speed'
String get playbackSpeed => 'Playback Speed'; String get playbackSpeed => 'Playback Speed';
/// en: 'Zoom'
String get zoom => 'Zoom';
/// en: 'Sleep Timer' /// en: 'Sleep Timer'
String get sleepTimer => 'Sleep Timer'; String get sleepTimer => 'Sleep Timer';
@@ -3680,6 +3686,15 @@ class TranslationsHotkeysActionsEn {
/// en: 'Reset Speed' /// en: 'Reset Speed'
String get speedReset => 'Reset Speed'; String get speedReset => 'Reset Speed';
/// en: 'Zoom In'
String get zoomIn => 'Zoom In';
/// en: 'Zoom Out'
String get zoomOut => 'Zoom Out';
/// en: 'Reset Zoom'
String get zoomReset => 'Reset Zoom';
/// en: 'Seek to Next Subtitle' /// en: 'Seek to Next Subtitle'
String get subSeekNext => 'Seek to Next Subtitle'; String get subSeekNext => 'Seek to Next Subtitle';
@@ -4463,6 +4478,9 @@ extension on Translations {
'hotkeys.actions.speedIncrease' => 'Increase Speed', 'hotkeys.actions.speedIncrease' => 'Increase Speed',
'hotkeys.actions.speedDecrease' => 'Decrease Speed', 'hotkeys.actions.speedDecrease' => 'Decrease Speed',
'hotkeys.actions.speedReset' => 'Reset Speed', 'hotkeys.actions.speedReset' => 'Reset Speed',
'hotkeys.actions.zoomIn' => 'Zoom In',
'hotkeys.actions.zoomOut' => 'Zoom Out',
'hotkeys.actions.zoomReset' => 'Reset Zoom',
'hotkeys.actions.subSeekNext' => 'Seek to Next Subtitle', 'hotkeys.actions.subSeekNext' => 'Seek to Next Subtitle',
'hotkeys.actions.subSeekPrev' => 'Seek to Previous Subtitle', 'hotkeys.actions.subSeekPrev' => 'Seek to Previous Subtitle',
'hotkeys.actions.shaderToggle' => 'Toggle Shaders', 'hotkeys.actions.shaderToggle' => 'Toggle Shaders',
@@ -4588,6 +4606,7 @@ extension on Translations {
'videoControls.pipActive' => 'Playing in Picture-in-Picture', 'videoControls.pipActive' => 'Playing in Picture-in-Picture',
'videoControls.pipFailed' => 'Picture-in-picture failed to start', 'videoControls.pipFailed' => 'Picture-in-picture failed to start',
'videoControls.screenshotSaved' => 'Screenshot saved', 'videoControls.screenshotSaved' => 'Screenshot saved',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Requires Android 8.0 or newer', 'videoControls.pipErrors.androidVersion' => 'Requires Android 8.0 or newer',
'videoControls.pipErrors.iosVersion' => 'Requires iOS 15.0 or newer', 'videoControls.pipErrors.iosVersion' => 'Requires iOS 15.0 or newer',
'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture is disabled. Enable it in system settings.', 'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture is disabled. Enable it in system settings.',
@@ -4680,12 +4699,12 @@ extension on Translations {
'profiles.addPlezyProfile' => 'Add Plezy profile', 'profiles.addPlezyProfile' => 'Add Plezy profile',
'profiles.switchingProfile' => 'Switching profile…', 'profiles.switchingProfile' => 'Switching profile…',
'profiles.deleteThisProfileTitle' => 'Delete this profile?', 'profiles.deleteThisProfileTitle' => 'Delete this profile?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Remove ${displayName}. Connections aren\'t affected.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Remove ${displayName}. Connections aren\'t affected.',
'profiles.active' => 'Active', 'profiles.active' => 'Active',
'profiles.manage' => 'Manage', 'profiles.manage' => 'Manage',
'profiles.delete' => 'Delete', 'profiles.delete' => 'Delete',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Sign out', 'profiles.signOut' => 'Sign out',
'profiles.signOutPlexTitle' => 'Sign out of Plex?', 'profiles.signOutPlexTitle' => 'Sign out of Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Remove ${displayName} and all Plex Home users? Sign back in anytime.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Remove ${displayName} and all Plex Home users? Sign back in anytime.',
@@ -5165,6 +5184,7 @@ extension on Translations {
'companionRemote.remote.audio' => 'Audio', 'companionRemote.remote.audio' => 'Audio',
'companionRemote.remote.searchHint' => 'Search on desktop...', 'companionRemote.remote.searchHint' => 'Search on desktop...',
'videoSettings.playbackSpeed' => 'Playback Speed', 'videoSettings.playbackSpeed' => 'Playback Speed',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Sleep Timer', 'videoSettings.sleepTimer' => 'Sleep Timer',
'videoSettings.audioSync' => 'Audio Sync', 'videoSettings.audioSync' => 'Audio Sync',
'videoSettings.subtitleSync' => 'Subtitle Sync', 'videoSettings.subtitleSync' => 'Subtitle Sync',
@@ -5193,13 +5213,13 @@ extension on Translations {
'metadataEdit.basicInfo' => 'Basic Info', 'metadataEdit.basicInfo' => 'Basic Info',
'metadataEdit.artwork' => 'Artwork', 'metadataEdit.artwork' => 'Artwork',
'metadataEdit.advancedSettings' => 'Advanced Settings', 'metadataEdit.advancedSettings' => 'Advanced Settings',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Title', 'metadataEdit.title' => 'Title',
'metadataEdit.sortTitle' => 'Sort Title', 'metadataEdit.sortTitle' => 'Sort Title',
'metadataEdit.originalTitle' => 'Original Title', 'metadataEdit.originalTitle' => 'Original Title',
'metadataEdit.releaseDate' => 'Release Date', 'metadataEdit.releaseDate' => 'Release Date',
'metadataEdit.contentRating' => 'Content Rating', 'metadataEdit.contentRating' => 'Content Rating',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studio', 'metadataEdit.studio' => 'Studio',
'metadataEdit.tagline' => 'Tagline', 'metadataEdit.tagline' => 'Tagline',
'metadataEdit.summary' => 'Summary', 'metadataEdit.summary' => 'Summary',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsEs extends TranslationsVideoControlsEn {
@override String get pipActive => 'Reproduciendo en Imagen en Imagen'; @override String get pipActive => 'Reproduciendo en Imagen en Imagen';
@override String get pipFailed => 'Error al iniciar Imagen en Imagen'; @override String get pipFailed => 'Error al iniciar Imagen en Imagen';
@override String get screenshotSaved => 'Captura de pantalla guardada'; @override String get screenshotSaved => 'Captura de pantalla guardada';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsEs pipErrors = _TranslationsVideoControlsPipErrorsEs._(_root); @override late final _TranslationsVideoControlsPipErrorsEs pipErrors = _TranslationsVideoControlsPipErrorsEs._(_root);
@override String get chapters => 'Capítulos'; @override String get chapters => 'Capítulos';
@override String get noChaptersAvailable => 'No hay capítulos disponibles'; @override String get noChaptersAvailable => 'No hay capítulos disponibles';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsEs extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Velocidad de reproducción'; @override String get playbackSpeed => 'Velocidad de reproducción';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Temporizador de apagado'; @override String get sleepTimer => 'Temporizador de apagado';
@override String get audioSync => 'Sincronización de audio'; @override String get audioSync => 'Sincronización de audio';
@override String get subtitleSync => 'Sincronización de subtítulos'; @override String get subtitleSync => 'Sincronización de subtítulos';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsEs extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Aumentar Velocidad'; @override String get speedIncrease => 'Aumentar Velocidad';
@override String get speedDecrease => 'Disminuir Velocidad'; @override String get speedDecrease => 'Disminuir Velocidad';
@override String get speedReset => 'Restablecer Velocidad'; @override String get speedReset => 'Restablecer Velocidad';
@override String get zoomIn => 'Acercar';
@override String get zoomOut => 'Alejar';
@override String get zoomReset => 'Restablecer zoom';
@override String get subSeekNext => 'Ir al Siguiente Subtítulo'; @override String get subSeekNext => 'Ir al Siguiente Subtítulo';
@override String get subSeekPrev => 'Ir al Anterior Subtítulo'; @override String get subSeekPrev => 'Ir al Anterior Subtítulo';
@override String get shaderToggle => 'Alternar Shaders'; @override String get shaderToggle => 'Alternar Shaders';
@@ -2099,6 +2104,9 @@ extension on TranslationsEs {
'hotkeys.actions.speedIncrease' => 'Aumentar Velocidad', 'hotkeys.actions.speedIncrease' => 'Aumentar Velocidad',
'hotkeys.actions.speedDecrease' => 'Disminuir Velocidad', 'hotkeys.actions.speedDecrease' => 'Disminuir Velocidad',
'hotkeys.actions.speedReset' => 'Restablecer Velocidad', 'hotkeys.actions.speedReset' => 'Restablecer Velocidad',
'hotkeys.actions.zoomIn' => 'Acercar',
'hotkeys.actions.zoomOut' => 'Alejar',
'hotkeys.actions.zoomReset' => 'Restablecer zoom',
'hotkeys.actions.subSeekNext' => 'Ir al Siguiente Subtítulo', 'hotkeys.actions.subSeekNext' => 'Ir al Siguiente Subtítulo',
'hotkeys.actions.subSeekPrev' => 'Ir al Anterior Subtítulo', 'hotkeys.actions.subSeekPrev' => 'Ir al Anterior Subtítulo',
'hotkeys.actions.shaderToggle' => 'Alternar Shaders', 'hotkeys.actions.shaderToggle' => 'Alternar Shaders',
@@ -2224,6 +2232,7 @@ extension on TranslationsEs {
'videoControls.pipActive' => 'Reproduciendo en Imagen en Imagen', 'videoControls.pipActive' => 'Reproduciendo en Imagen en Imagen',
'videoControls.pipFailed' => 'Error al iniciar Imagen en Imagen', 'videoControls.pipFailed' => 'Error al iniciar Imagen en Imagen',
'videoControls.screenshotSaved' => 'Captura de pantalla guardada', 'videoControls.screenshotSaved' => 'Captura de pantalla guardada',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Requiere Android 8.0 o más reciente', 'videoControls.pipErrors.androidVersion' => 'Requiere Android 8.0 o más reciente',
'videoControls.pipErrors.iosVersion' => 'Requiere iOS 15.0 o más reciente', 'videoControls.pipErrors.iosVersion' => 'Requiere iOS 15.0 o más reciente',
'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture está desactivado. Actívalo en los ajustes del sistema.', 'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture está desactivado. Actívalo en los ajustes del sistema.',
@@ -2316,12 +2325,12 @@ extension on TranslationsEs {
'profiles.addPlezyProfile' => 'Añadir perfil de Plezy', 'profiles.addPlezyProfile' => 'Añadir perfil de Plezy',
'profiles.switchingProfile' => 'Cambiando de perfil…', 'profiles.switchingProfile' => 'Cambiando de perfil…',
'profiles.deleteThisProfileTitle' => '¿Eliminar este perfil?', 'profiles.deleteThisProfileTitle' => '¿Eliminar este perfil?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Eliminar ${displayName}. Las conexiones no se verán afectadas.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Eliminar ${displayName}. Las conexiones no se verán afectadas.',
'profiles.active' => 'Activo', 'profiles.active' => 'Activo',
'profiles.manage' => 'Administrar', 'profiles.manage' => 'Administrar',
'profiles.delete' => 'Eliminar', 'profiles.delete' => 'Eliminar',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Cerrar sesión', 'profiles.signOut' => 'Cerrar sesión',
'profiles.signOutPlexTitle' => '¿Cerrar sesión de Plex?', 'profiles.signOutPlexTitle' => '¿Cerrar sesión de Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => '¿Eliminar ${displayName} y todos los usuarios de Plex Home? Puedes iniciar sesión de nuevo cuando quieras.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => '¿Eliminar ${displayName} y todos los usuarios de Plex Home? Puedes iniciar sesión de nuevo cuando quieras.',
@@ -2801,6 +2810,7 @@ extension on TranslationsEs {
'companionRemote.remote.audio' => 'Audio', 'companionRemote.remote.audio' => 'Audio',
'companionRemote.remote.searchHint' => 'Buscar en escritorio...', 'companionRemote.remote.searchHint' => 'Buscar en escritorio...',
'videoSettings.playbackSpeed' => 'Velocidad de reproducción', 'videoSettings.playbackSpeed' => 'Velocidad de reproducción',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Temporizador de apagado', 'videoSettings.sleepTimer' => 'Temporizador de apagado',
'videoSettings.audioSync' => 'Sincronización de audio', 'videoSettings.audioSync' => 'Sincronización de audio',
'videoSettings.subtitleSync' => 'Sincronización de subtítulos', 'videoSettings.subtitleSync' => 'Sincronización de subtítulos',
@@ -2829,13 +2839,13 @@ extension on TranslationsEs {
'metadataEdit.basicInfo' => 'Información básica', 'metadataEdit.basicInfo' => 'Información básica',
'metadataEdit.artwork' => 'Artwork', 'metadataEdit.artwork' => 'Artwork',
'metadataEdit.advancedSettings' => 'Ajustes avanzados', 'metadataEdit.advancedSettings' => 'Ajustes avanzados',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Título', 'metadataEdit.title' => 'Título',
'metadataEdit.sortTitle' => 'Título de ordenación', 'metadataEdit.sortTitle' => 'Título de ordenación',
'metadataEdit.originalTitle' => 'Título original', 'metadataEdit.originalTitle' => 'Título original',
'metadataEdit.releaseDate' => 'Fecha de estreno', 'metadataEdit.releaseDate' => 'Fecha de estreno',
'metadataEdit.contentRating' => 'Clasificación de contenido', 'metadataEdit.contentRating' => 'Clasificación de contenido',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Estudio', 'metadataEdit.studio' => 'Estudio',
'metadataEdit.tagline' => 'Eslogan', 'metadataEdit.tagline' => 'Eslogan',
'metadataEdit.summary' => 'Resumen', 'metadataEdit.summary' => 'Resumen',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsFr extends TranslationsVideoControlsEn {
@override String get pipActive => 'Lecture en mode image dans l\'image'; @override String get pipActive => 'Lecture en mode image dans l\'image';
@override String get pipFailed => 'Échec du démarrage du mode image dans l\'image'; @override String get pipFailed => 'Échec du démarrage du mode image dans l\'image';
@override String get screenshotSaved => 'Capture d\'écran enregistrée'; @override String get screenshotSaved => 'Capture d\'écran enregistrée';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent} %';
@override late final _TranslationsVideoControlsPipErrorsFr pipErrors = _TranslationsVideoControlsPipErrorsFr._(_root); @override late final _TranslationsVideoControlsPipErrorsFr pipErrors = _TranslationsVideoControlsPipErrorsFr._(_root);
@override String get chapters => 'Chapitres'; @override String get chapters => 'Chapitres';
@override String get noChaptersAvailable => 'Aucun chapitre disponible'; @override String get noChaptersAvailable => 'Aucun chapitre disponible';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsFr extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Vitesse de lecture'; @override String get playbackSpeed => 'Vitesse de lecture';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Minuterie de mise en veille'; @override String get sleepTimer => 'Minuterie de mise en veille';
@override String get audioSync => 'Synchronisation audio'; @override String get audioSync => 'Synchronisation audio';
@override String get subtitleSync => 'Synchronisation des sous-titres'; @override String get subtitleSync => 'Synchronisation des sous-titres';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsFr extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Augmenter la vitesse'; @override String get speedIncrease => 'Augmenter la vitesse';
@override String get speedDecrease => 'Réduire la vitesse'; @override String get speedDecrease => 'Réduire la vitesse';
@override String get speedReset => 'Réinitialiser la vitesse'; @override String get speedReset => 'Réinitialiser la vitesse';
@override String get zoomIn => 'Zoom avant';
@override String get zoomOut => 'Zoom arrière';
@override String get zoomReset => 'Réinitialiser le zoom';
@override String get subSeekNext => 'Rechercher le sous-titre suivant'; @override String get subSeekNext => 'Rechercher le sous-titre suivant';
@override String get subSeekPrev => 'Rechercher le sous-titre précédent'; @override String get subSeekPrev => 'Rechercher le sous-titre précédent';
@override String get shaderToggle => 'Activer/désactiver les shaders'; @override String get shaderToggle => 'Activer/désactiver les shaders';
@@ -2099,6 +2104,9 @@ extension on TranslationsFr {
'hotkeys.actions.speedIncrease' => 'Augmenter la vitesse', 'hotkeys.actions.speedIncrease' => 'Augmenter la vitesse',
'hotkeys.actions.speedDecrease' => 'Réduire la vitesse', 'hotkeys.actions.speedDecrease' => 'Réduire la vitesse',
'hotkeys.actions.speedReset' => 'Réinitialiser la vitesse', 'hotkeys.actions.speedReset' => 'Réinitialiser la vitesse',
'hotkeys.actions.zoomIn' => 'Zoom avant',
'hotkeys.actions.zoomOut' => 'Zoom arrière',
'hotkeys.actions.zoomReset' => 'Réinitialiser le zoom',
'hotkeys.actions.subSeekNext' => 'Rechercher le sous-titre suivant', 'hotkeys.actions.subSeekNext' => 'Rechercher le sous-titre suivant',
'hotkeys.actions.subSeekPrev' => 'Rechercher le sous-titre précédent', 'hotkeys.actions.subSeekPrev' => 'Rechercher le sous-titre précédent',
'hotkeys.actions.shaderToggle' => 'Activer/désactiver les shaders', 'hotkeys.actions.shaderToggle' => 'Activer/désactiver les shaders',
@@ -2224,6 +2232,7 @@ extension on TranslationsFr {
'videoControls.pipActive' => 'Lecture en mode image dans l\'image', 'videoControls.pipActive' => 'Lecture en mode image dans l\'image',
'videoControls.pipFailed' => 'Échec du démarrage du mode image dans l\'image', 'videoControls.pipFailed' => 'Échec du démarrage du mode image dans l\'image',
'videoControls.screenshotSaved' => 'Capture d\'écran enregistrée', 'videoControls.screenshotSaved' => 'Capture d\'écran enregistrée',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent} %',
'videoControls.pipErrors.androidVersion' => 'Nécessite Android 8.0 ou plus récent', 'videoControls.pipErrors.androidVersion' => 'Nécessite Android 8.0 ou plus récent',
'videoControls.pipErrors.iosVersion' => 'Nécessite iOS 15.0 ou plus récent', 'videoControls.pipErrors.iosVersion' => 'Nécessite iOS 15.0 ou plus récent',
'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture est désactivé. Activez-le dans les paramètres système.', 'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture est désactivé. Activez-le dans les paramètres système.',
@@ -2316,12 +2325,12 @@ extension on TranslationsFr {
'profiles.addPlezyProfile' => 'Ajouter un profil Plezy', 'profiles.addPlezyProfile' => 'Ajouter un profil Plezy',
'profiles.switchingProfile' => 'Changement de profil…', 'profiles.switchingProfile' => 'Changement de profil…',
'profiles.deleteThisProfileTitle' => 'Supprimer ce profil ?', 'profiles.deleteThisProfileTitle' => 'Supprimer ce profil ?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Supprimer ${displayName}. Les connexions ne sont pas affectées.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Supprimer ${displayName}. Les connexions ne sont pas affectées.',
'profiles.active' => 'Actif', 'profiles.active' => 'Actif',
'profiles.manage' => 'Gérer', 'profiles.manage' => 'Gérer',
'profiles.delete' => 'Supprimer', 'profiles.delete' => 'Supprimer',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Se déconnecter', 'profiles.signOut' => 'Se déconnecter',
'profiles.signOutPlexTitle' => 'Se déconnecter de Plex ?', 'profiles.signOutPlexTitle' => 'Se déconnecter de Plex ?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Supprimer ${displayName} et tous les utilisateurs Plex Home ? Reconnexion possible à tout moment.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Supprimer ${displayName} et tous les utilisateurs Plex Home ? Reconnexion possible à tout moment.',
@@ -2801,6 +2810,7 @@ extension on TranslationsFr {
'companionRemote.remote.audio' => 'Audio', 'companionRemote.remote.audio' => 'Audio',
'companionRemote.remote.searchHint' => 'Rechercher sur le bureau...', 'companionRemote.remote.searchHint' => 'Rechercher sur le bureau...',
'videoSettings.playbackSpeed' => 'Vitesse de lecture', 'videoSettings.playbackSpeed' => 'Vitesse de lecture',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Minuterie de mise en veille', 'videoSettings.sleepTimer' => 'Minuterie de mise en veille',
'videoSettings.audioSync' => 'Synchronisation audio', 'videoSettings.audioSync' => 'Synchronisation audio',
'videoSettings.subtitleSync' => 'Synchronisation des sous-titres', 'videoSettings.subtitleSync' => 'Synchronisation des sous-titres',
@@ -2829,13 +2839,13 @@ extension on TranslationsFr {
'metadataEdit.basicInfo' => 'Informations de base', 'metadataEdit.basicInfo' => 'Informations de base',
'metadataEdit.artwork' => 'Artwork', 'metadataEdit.artwork' => 'Artwork',
'metadataEdit.advancedSettings' => 'Paramètres avancés', 'metadataEdit.advancedSettings' => 'Paramètres avancés',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Titre', 'metadataEdit.title' => 'Titre',
'metadataEdit.sortTitle' => 'Titre de tri', 'metadataEdit.sortTitle' => 'Titre de tri',
'metadataEdit.originalTitle' => 'Titre original', 'metadataEdit.originalTitle' => 'Titre original',
'metadataEdit.releaseDate' => 'Date de sortie', 'metadataEdit.releaseDate' => 'Date de sortie',
'metadataEdit.contentRating' => 'Classification', 'metadataEdit.contentRating' => 'Classification',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studio', 'metadataEdit.studio' => 'Studio',
'metadataEdit.tagline' => 'Slogan', 'metadataEdit.tagline' => 'Slogan',
'metadataEdit.summary' => 'Résumé', 'metadataEdit.summary' => 'Résumé',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsIt extends TranslationsVideoControlsEn {
@override String get pipActive => 'Riproduzione in Picture-in-Picture'; @override String get pipActive => 'Riproduzione in Picture-in-Picture';
@override String get pipFailed => 'Impossibile avviare la modalità Picture-in-Picture'; @override String get pipFailed => 'Impossibile avviare la modalità Picture-in-Picture';
@override String get screenshotSaved => 'Schermata salvata'; @override String get screenshotSaved => 'Schermata salvata';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsIt pipErrors = _TranslationsVideoControlsPipErrorsIt._(_root); @override late final _TranslationsVideoControlsPipErrorsIt pipErrors = _TranslationsVideoControlsPipErrorsIt._(_root);
@override String get chapters => 'Capitoli'; @override String get chapters => 'Capitoli';
@override String get noChaptersAvailable => 'Nessun capitolo disponibile'; @override String get noChaptersAvailable => 'Nessun capitolo disponibile';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsIt extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Velocità di riproduzione'; @override String get playbackSpeed => 'Velocità di riproduzione';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Timer di spegnimento'; @override String get sleepTimer => 'Timer di spegnimento';
@override String get audioSync => 'Sincronizzazione audio'; @override String get audioSync => 'Sincronizzazione audio';
@override String get subtitleSync => 'Sincronizzazione sottotitoli'; @override String get subtitleSync => 'Sincronizzazione sottotitoli';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsIt extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Aumenta velocità'; @override String get speedIncrease => 'Aumenta velocità';
@override String get speedDecrease => 'Diminuisci velocità'; @override String get speedDecrease => 'Diminuisci velocità';
@override String get speedReset => 'Ripristina velocità'; @override String get speedReset => 'Ripristina velocità';
@override String get zoomIn => 'Aumenta zoom';
@override String get zoomOut => 'Riduci zoom';
@override String get zoomReset => 'Ripristina zoom';
@override String get subSeekNext => 'Vai al sottotitolo successivo'; @override String get subSeekNext => 'Vai al sottotitolo successivo';
@override String get subSeekPrev => 'Vai al sottotitolo precedente'; @override String get subSeekPrev => 'Vai al sottotitolo precedente';
@override String get shaderToggle => 'Attiva/disattiva shader'; @override String get shaderToggle => 'Attiva/disattiva shader';
@@ -2099,6 +2104,9 @@ extension on TranslationsIt {
'hotkeys.actions.speedIncrease' => 'Aumenta velocità', 'hotkeys.actions.speedIncrease' => 'Aumenta velocità',
'hotkeys.actions.speedDecrease' => 'Diminuisci velocità', 'hotkeys.actions.speedDecrease' => 'Diminuisci velocità',
'hotkeys.actions.speedReset' => 'Ripristina velocità', 'hotkeys.actions.speedReset' => 'Ripristina velocità',
'hotkeys.actions.zoomIn' => 'Aumenta zoom',
'hotkeys.actions.zoomOut' => 'Riduci zoom',
'hotkeys.actions.zoomReset' => 'Ripristina zoom',
'hotkeys.actions.subSeekNext' => 'Vai al sottotitolo successivo', 'hotkeys.actions.subSeekNext' => 'Vai al sottotitolo successivo',
'hotkeys.actions.subSeekPrev' => 'Vai al sottotitolo precedente', 'hotkeys.actions.subSeekPrev' => 'Vai al sottotitolo precedente',
'hotkeys.actions.shaderToggle' => 'Attiva/disattiva shader', 'hotkeys.actions.shaderToggle' => 'Attiva/disattiva shader',
@@ -2224,6 +2232,7 @@ extension on TranslationsIt {
'videoControls.pipActive' => 'Riproduzione in Picture-in-Picture', 'videoControls.pipActive' => 'Riproduzione in Picture-in-Picture',
'videoControls.pipFailed' => 'Impossibile avviare la modalità Picture-in-Picture', 'videoControls.pipFailed' => 'Impossibile avviare la modalità Picture-in-Picture',
'videoControls.screenshotSaved' => 'Schermata salvata', 'videoControls.screenshotSaved' => 'Schermata salvata',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Richiede Android 8.0 o versioni successive', 'videoControls.pipErrors.androidVersion' => 'Richiede Android 8.0 o versioni successive',
'videoControls.pipErrors.iosVersion' => 'Richiede iOS 15.0 o versioni successive', 'videoControls.pipErrors.iosVersion' => 'Richiede iOS 15.0 o versioni successive',
'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture è disattivato. Attivalo nelle impostazioni di sistema.', 'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture è disattivato. Attivalo nelle impostazioni di sistema.',
@@ -2316,12 +2325,12 @@ extension on TranslationsIt {
'profiles.addPlezyProfile' => 'Aggiungi profilo Plezy', 'profiles.addPlezyProfile' => 'Aggiungi profilo Plezy',
'profiles.switchingProfile' => 'Cambio profilo…', 'profiles.switchingProfile' => 'Cambio profilo…',
'profiles.deleteThisProfileTitle' => 'Eliminare questo profilo?', 'profiles.deleteThisProfileTitle' => 'Eliminare questo profilo?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Rimuovi ${displayName}. Le connessioni non sono interessate.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Rimuovi ${displayName}. Le connessioni non sono interessate.',
'profiles.active' => 'Attivo', 'profiles.active' => 'Attivo',
'profiles.manage' => 'Gestisci', 'profiles.manage' => 'Gestisci',
'profiles.delete' => 'Elimina', 'profiles.delete' => 'Elimina',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Esci', 'profiles.signOut' => 'Esci',
'profiles.signOutPlexTitle' => 'Uscire da Plex?', 'profiles.signOutPlexTitle' => 'Uscire da Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Rimuovere ${displayName} e tutti gli utenti Plex Home? Puoi accedere di nuovo quando vuoi.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Rimuovere ${displayName} e tutti gli utenti Plex Home? Puoi accedere di nuovo quando vuoi.',
@@ -2801,6 +2810,7 @@ extension on TranslationsIt {
'companionRemote.remote.audio' => 'Audio', 'companionRemote.remote.audio' => 'Audio',
'companionRemote.remote.searchHint' => 'Cerca sul desktop...', 'companionRemote.remote.searchHint' => 'Cerca sul desktop...',
'videoSettings.playbackSpeed' => 'Velocità di riproduzione', 'videoSettings.playbackSpeed' => 'Velocità di riproduzione',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Timer di spegnimento', 'videoSettings.sleepTimer' => 'Timer di spegnimento',
'videoSettings.audioSync' => 'Sincronizzazione audio', 'videoSettings.audioSync' => 'Sincronizzazione audio',
'videoSettings.subtitleSync' => 'Sincronizzazione sottotitoli', 'videoSettings.subtitleSync' => 'Sincronizzazione sottotitoli',
@@ -2829,13 +2839,13 @@ extension on TranslationsIt {
'metadataEdit.basicInfo' => 'Informazioni di base', 'metadataEdit.basicInfo' => 'Informazioni di base',
'metadataEdit.artwork' => 'Artwork', 'metadataEdit.artwork' => 'Artwork',
'metadataEdit.advancedSettings' => 'Impostazioni avanzate', 'metadataEdit.advancedSettings' => 'Impostazioni avanzate',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Titolo', 'metadataEdit.title' => 'Titolo',
'metadataEdit.sortTitle' => 'Titolo di ordinamento', 'metadataEdit.sortTitle' => 'Titolo di ordinamento',
'metadataEdit.originalTitle' => 'Titolo originale', 'metadataEdit.originalTitle' => 'Titolo originale',
'metadataEdit.releaseDate' => 'Data di uscita', 'metadataEdit.releaseDate' => 'Data di uscita',
'metadataEdit.contentRating' => 'Classificazione', 'metadataEdit.contentRating' => 'Classificazione',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studio', 'metadataEdit.studio' => 'Studio',
'metadataEdit.tagline' => 'Tagline', 'metadataEdit.tagline' => 'Tagline',
'metadataEdit.summary' => 'Trama', 'metadataEdit.summary' => 'Trama',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsJa extends TranslationsVideoControlsEn {
@override String get pipActive => 'ピクチャーインピクチャーで再生中'; @override String get pipActive => 'ピクチャーインピクチャーで再生中';
@override String get pipFailed => 'ピクチャーインピクチャーの開始に失敗しました'; @override String get pipFailed => 'ピクチャーインピクチャーの開始に失敗しました';
@override String get screenshotSaved => 'スクリーンショットを保存しました'; @override String get screenshotSaved => 'スクリーンショットを保存しました';
@override String zoomPercent({required Object percent}) => 'ズーム ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsJa pipErrors = _TranslationsVideoControlsPipErrorsJa._(_root); @override late final _TranslationsVideoControlsPipErrorsJa pipErrors = _TranslationsVideoControlsPipErrorsJa._(_root);
@override String get chapters => 'チャプター'; @override String get chapters => 'チャプター';
@override String get noChaptersAvailable => 'チャプターがありません'; @override String get noChaptersAvailable => 'チャプターがありません';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsJa extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => '再生速度'; @override String get playbackSpeed => '再生速度';
@override String get zoom => 'ズーム';
@override String get sleepTimer => 'スリープタイマー'; @override String get sleepTimer => 'スリープタイマー';
@override String get audioSync => '音声同期'; @override String get audioSync => '音声同期';
@override String get subtitleSync => '字幕同期'; @override String get subtitleSync => '字幕同期';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsJa extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => '速度を上げる'; @override String get speedIncrease => '速度を上げる';
@override String get speedDecrease => '速度を下げる'; @override String get speedDecrease => '速度を下げる';
@override String get speedReset => '速度をリセット'; @override String get speedReset => '速度をリセット';
@override String get zoomIn => 'ズームイン';
@override String get zoomOut => 'ズームアウト';
@override String get zoomReset => 'ズームをリセット';
@override String get subSeekNext => '次の字幕にシーク'; @override String get subSeekNext => '次の字幕にシーク';
@override String get subSeekPrev => '前の字幕にシーク'; @override String get subSeekPrev => '前の字幕にシーク';
@override String get shaderToggle => 'シェーダー切替'; @override String get shaderToggle => 'シェーダー切替';
@@ -2099,6 +2104,9 @@ extension on TranslationsJa {
'hotkeys.actions.speedIncrease' => '速度を上げる', 'hotkeys.actions.speedIncrease' => '速度を上げる',
'hotkeys.actions.speedDecrease' => '速度を下げる', 'hotkeys.actions.speedDecrease' => '速度を下げる',
'hotkeys.actions.speedReset' => '速度をリセット', 'hotkeys.actions.speedReset' => '速度をリセット',
'hotkeys.actions.zoomIn' => 'ズームイン',
'hotkeys.actions.zoomOut' => 'ズームアウト',
'hotkeys.actions.zoomReset' => 'ズームをリセット',
'hotkeys.actions.subSeekNext' => '次の字幕にシーク', 'hotkeys.actions.subSeekNext' => '次の字幕にシーク',
'hotkeys.actions.subSeekPrev' => '前の字幕にシーク', 'hotkeys.actions.subSeekPrev' => '前の字幕にシーク',
'hotkeys.actions.shaderToggle' => 'シェーダー切替', 'hotkeys.actions.shaderToggle' => 'シェーダー切替',
@@ -2224,6 +2232,7 @@ extension on TranslationsJa {
'videoControls.pipActive' => 'ピクチャーインピクチャーで再生中', 'videoControls.pipActive' => 'ピクチャーインピクチャーで再生中',
'videoControls.pipFailed' => 'ピクチャーインピクチャーの開始に失敗しました', 'videoControls.pipFailed' => 'ピクチャーインピクチャーの開始に失敗しました',
'videoControls.screenshotSaved' => 'スクリーンショットを保存しました', 'videoControls.screenshotSaved' => 'スクリーンショットを保存しました',
'videoControls.zoomPercent' => ({required Object percent}) => 'ズーム ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Android 8.0以降が必要です', 'videoControls.pipErrors.androidVersion' => 'Android 8.0以降が必要です',
'videoControls.pipErrors.iosVersion' => 'iOS 15.0以降が必要です', 'videoControls.pipErrors.iosVersion' => 'iOS 15.0以降が必要です',
'videoControls.pipErrors.permissionDisabled' => 'ピクチャーインピクチャーが無効です。システム設定で有効にしてください。', 'videoControls.pipErrors.permissionDisabled' => 'ピクチャーインピクチャーが無効です。システム設定で有効にしてください。',
@@ -2316,12 +2325,12 @@ extension on TranslationsJa {
'profiles.addPlezyProfile' => 'Plezyプロファイルを追加', 'profiles.addPlezyProfile' => 'Plezyプロファイルを追加',
'profiles.switchingProfile' => 'プロファイルを切り替え中…', 'profiles.switchingProfile' => 'プロファイルを切り替え中…',
'profiles.deleteThisProfileTitle' => 'このプロファイルを削除しますか?', 'profiles.deleteThisProfileTitle' => 'このプロファイルを削除しますか?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName}を削除します。接続には影響しません。', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName}を削除します。接続には影響しません。',
'profiles.active' => 'アクティブ', 'profiles.active' => 'アクティブ',
'profiles.manage' => '管理', 'profiles.manage' => '管理',
'profiles.delete' => '削除', 'profiles.delete' => '削除',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'サインアウト', 'profiles.signOut' => 'サインアウト',
'profiles.signOutPlexTitle' => 'Plex からサインアウトしますか?', 'profiles.signOutPlexTitle' => 'Plex からサインアウトしますか?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName}とすべてのPlex Homeユーザーを削除しますか?いつでも再サインインできます。', 'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName}とすべてのPlex Homeユーザーを削除しますか?いつでも再サインインできます。',
@@ -2801,6 +2810,7 @@ extension on TranslationsJa {
'companionRemote.remote.audio' => '音声', 'companionRemote.remote.audio' => '音声',
'companionRemote.remote.searchHint' => 'デスクトップで検索...', 'companionRemote.remote.searchHint' => 'デスクトップで検索...',
'videoSettings.playbackSpeed' => '再生速度', 'videoSettings.playbackSpeed' => '再生速度',
'videoSettings.zoom' => 'ズーム',
'videoSettings.sleepTimer' => 'スリープタイマー', 'videoSettings.sleepTimer' => 'スリープタイマー',
'videoSettings.audioSync' => '音声同期', 'videoSettings.audioSync' => '音声同期',
'videoSettings.subtitleSync' => '字幕同期', 'videoSettings.subtitleSync' => '字幕同期',
@@ -2829,13 +2839,13 @@ extension on TranslationsJa {
'metadataEdit.basicInfo' => '基本情報', 'metadataEdit.basicInfo' => '基本情報',
'metadataEdit.artwork' => 'アートワーク', 'metadataEdit.artwork' => 'アートワーク',
'metadataEdit.advancedSettings' => '詳細設定', 'metadataEdit.advancedSettings' => '詳細設定',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'タイトル', 'metadataEdit.title' => 'タイトル',
'metadataEdit.sortTitle' => 'ソートタイトル', 'metadataEdit.sortTitle' => 'ソートタイトル',
'metadataEdit.originalTitle' => '原題', 'metadataEdit.originalTitle' => '原題',
'metadataEdit.releaseDate' => '公開日', 'metadataEdit.releaseDate' => '公開日',
'metadataEdit.contentRating' => 'コンテンツレーティング', 'metadataEdit.contentRating' => 'コンテンツレーティング',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'スタジオ', 'metadataEdit.studio' => 'スタジオ',
'metadataEdit.tagline' => 'タグライン', 'metadataEdit.tagline' => 'タグライン',
'metadataEdit.summary' => 'あらすじ', 'metadataEdit.summary' => 'あらすじ',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsKo extends TranslationsVideoControlsEn {
@override String get pipActive => '화면 속 화면으로 재생 중'; @override String get pipActive => '화면 속 화면으로 재생 중';
@override String get pipFailed => '화면 속 화면 모드를 시작할 수 없습니다'; @override String get pipFailed => '화면 속 화면 모드를 시작할 수 없습니다';
@override String get screenshotSaved => '스크린샷 저장됨'; @override String get screenshotSaved => '스크린샷 저장됨';
@override String zoomPercent({required Object percent}) => '확대/축소 ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsKo pipErrors = _TranslationsVideoControlsPipErrorsKo._(_root); @override late final _TranslationsVideoControlsPipErrorsKo pipErrors = _TranslationsVideoControlsPipErrorsKo._(_root);
@override String get chapters => '챕터'; @override String get chapters => '챕터';
@override String get noChaptersAvailable => '사용 가능한 챕터가 없습니다'; @override String get noChaptersAvailable => '사용 가능한 챕터가 없습니다';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsKo extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => '재생 속도'; @override String get playbackSpeed => '재생 속도';
@override String get zoom => '확대/축소';
@override String get sleepTimer => '취침 타이머'; @override String get sleepTimer => '취침 타이머';
@override String get audioSync => '오디오 동기화'; @override String get audioSync => '오디오 동기화';
@override String get subtitleSync => '자막 동기화'; @override String get subtitleSync => '자막 동기화';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsKo extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => '속도 높이기'; @override String get speedIncrease => '속도 높이기';
@override String get speedDecrease => '속도 낮추기'; @override String get speedDecrease => '속도 낮추기';
@override String get speedReset => '속도 초기화'; @override String get speedReset => '속도 초기화';
@override String get zoomIn => '확대';
@override String get zoomOut => '축소';
@override String get zoomReset => '확대/축소 초기화';
@override String get subSeekNext => '다음 자막으로 이동'; @override String get subSeekNext => '다음 자막으로 이동';
@override String get subSeekPrev => '이전 자막으로 이동'; @override String get subSeekPrev => '이전 자막으로 이동';
@override String get shaderToggle => '셰이더 전환'; @override String get shaderToggle => '셰이더 전환';
@@ -2099,6 +2104,9 @@ extension on TranslationsKo {
'hotkeys.actions.speedIncrease' => '속도 높이기', 'hotkeys.actions.speedIncrease' => '속도 높이기',
'hotkeys.actions.speedDecrease' => '속도 낮추기', 'hotkeys.actions.speedDecrease' => '속도 낮추기',
'hotkeys.actions.speedReset' => '속도 초기화', 'hotkeys.actions.speedReset' => '속도 초기화',
'hotkeys.actions.zoomIn' => '확대',
'hotkeys.actions.zoomOut' => '축소',
'hotkeys.actions.zoomReset' => '확대/축소 초기화',
'hotkeys.actions.subSeekNext' => '다음 자막으로 이동', 'hotkeys.actions.subSeekNext' => '다음 자막으로 이동',
'hotkeys.actions.subSeekPrev' => '이전 자막으로 이동', 'hotkeys.actions.subSeekPrev' => '이전 자막으로 이동',
'hotkeys.actions.shaderToggle' => '셰이더 전환', 'hotkeys.actions.shaderToggle' => '셰이더 전환',
@@ -2224,6 +2232,7 @@ extension on TranslationsKo {
'videoControls.pipActive' => '화면 속 화면으로 재생 중', 'videoControls.pipActive' => '화면 속 화면으로 재생 중',
'videoControls.pipFailed' => '화면 속 화면 모드를 시작할 수 없습니다', 'videoControls.pipFailed' => '화면 속 화면 모드를 시작할 수 없습니다',
'videoControls.screenshotSaved' => '스크린샷 저장됨', 'videoControls.screenshotSaved' => '스크린샷 저장됨',
'videoControls.zoomPercent' => ({required Object percent}) => '확대/축소 ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Android 8.0 이상이 필요합니다', 'videoControls.pipErrors.androidVersion' => 'Android 8.0 이상이 필요합니다',
'videoControls.pipErrors.iosVersion' => 'iOS 15.0 이상이 필요합니다', 'videoControls.pipErrors.iosVersion' => 'iOS 15.0 이상이 필요합니다',
'videoControls.pipErrors.permissionDisabled' => '화면 속 화면이 비활성화되어 있습니다. 시스템 설정에서 활성화하세요.', 'videoControls.pipErrors.permissionDisabled' => '화면 속 화면이 비활성화되어 있습니다. 시스템 설정에서 활성화하세요.',
@@ -2316,12 +2325,12 @@ extension on TranslationsKo {
'profiles.addPlezyProfile' => 'Plezy 프로필 추가', 'profiles.addPlezyProfile' => 'Plezy 프로필 추가',
'profiles.switchingProfile' => '프로필 전환 중…', 'profiles.switchingProfile' => '프로필 전환 중…',
'profiles.deleteThisProfileTitle' => '이 프로필을 삭제하시겠습니까?', 'profiles.deleteThisProfileTitle' => '이 프로필을 삭제하시겠습니까?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName}을(를) 제거합니다. 연결에는 영향이 없습니다.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName}을(를) 제거합니다. 연결에는 영향이 없습니다.',
'profiles.active' => '활성', 'profiles.active' => '활성',
'profiles.manage' => '관리', 'profiles.manage' => '관리',
'profiles.delete' => '삭제', 'profiles.delete' => '삭제',
_ => null,
} ?? switch (path) {
'profiles.signOut' => '로그아웃', 'profiles.signOut' => '로그아웃',
'profiles.signOutPlexTitle' => 'Plex에서 로그아웃하시겠습니까?', 'profiles.signOutPlexTitle' => 'Plex에서 로그아웃하시겠습니까?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} 및 모든 Plex Home 사용자를 제거할까요? 언제든 다시 로그인할 수 있습니다.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} 및 모든 Plex Home 사용자를 제거할까요? 언제든 다시 로그인할 수 있습니다.',
@@ -2801,6 +2810,7 @@ extension on TranslationsKo {
'companionRemote.remote.audio' => '오디오', 'companionRemote.remote.audio' => '오디오',
'companionRemote.remote.searchHint' => '데스크톱에서 검색...', 'companionRemote.remote.searchHint' => '데스크톱에서 검색...',
'videoSettings.playbackSpeed' => '재생 속도', 'videoSettings.playbackSpeed' => '재생 속도',
'videoSettings.zoom' => '확대/축소',
'videoSettings.sleepTimer' => '취침 타이머', 'videoSettings.sleepTimer' => '취침 타이머',
'videoSettings.audioSync' => '오디오 동기화', 'videoSettings.audioSync' => '오디오 동기화',
'videoSettings.subtitleSync' => '자막 동기화', 'videoSettings.subtitleSync' => '자막 동기화',
@@ -2829,13 +2839,13 @@ extension on TranslationsKo {
'metadataEdit.basicInfo' => '기본 정보', 'metadataEdit.basicInfo' => '기본 정보',
'metadataEdit.artwork' => '아트워크', 'metadataEdit.artwork' => '아트워크',
'metadataEdit.advancedSettings' => '고급 설정', 'metadataEdit.advancedSettings' => '고급 설정',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => '제목', 'metadataEdit.title' => '제목',
'metadataEdit.sortTitle' => '정렬 제목', 'metadataEdit.sortTitle' => '정렬 제목',
'metadataEdit.originalTitle' => '원제', 'metadataEdit.originalTitle' => '원제',
'metadataEdit.releaseDate' => '출시일', 'metadataEdit.releaseDate' => '출시일',
'metadataEdit.contentRating' => '콘텐츠 등급', 'metadataEdit.contentRating' => '콘텐츠 등급',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => '스튜디오', 'metadataEdit.studio' => '스튜디오',
'metadataEdit.tagline' => '태그라인', 'metadataEdit.tagline' => '태그라인',
'metadataEdit.summary' => '줄거리', 'metadataEdit.summary' => '줄거리',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsNb extends TranslationsVideoControlsEn {
@override String get pipActive => 'Spiller i bilde-i-bilde'; @override String get pipActive => 'Spiller i bilde-i-bilde';
@override String get pipFailed => 'Bilde-i-bilde kunne ikke starte'; @override String get pipFailed => 'Bilde-i-bilde kunne ikke starte';
@override String get screenshotSaved => 'Skjermbilde lagret'; @override String get screenshotSaved => 'Skjermbilde lagret';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsNb pipErrors = _TranslationsVideoControlsPipErrorsNb._(_root); @override late final _TranslationsVideoControlsPipErrorsNb pipErrors = _TranslationsVideoControlsPipErrorsNb._(_root);
@override String get chapters => 'Kapitler'; @override String get chapters => 'Kapitler';
@override String get noChaptersAvailable => 'Ingen kapitler tilgjengelig'; @override String get noChaptersAvailable => 'Ingen kapitler tilgjengelig';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsNb extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Avspillingshastighet'; @override String get playbackSpeed => 'Avspillingshastighet';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Søvntimer'; @override String get sleepTimer => 'Søvntimer';
@override String get audioSync => 'Lydsynkronisering'; @override String get audioSync => 'Lydsynkronisering';
@override String get subtitleSync => 'Undertekstsynkronisering'; @override String get subtitleSync => 'Undertekstsynkronisering';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsNb extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Øk hastighet'; @override String get speedIncrease => 'Øk hastighet';
@override String get speedDecrease => 'Reduser hastighet'; @override String get speedDecrease => 'Reduser hastighet';
@override String get speedReset => 'Tilbakestill hastighet'; @override String get speedReset => 'Tilbakestill hastighet';
@override String get zoomIn => 'Zoom inn';
@override String get zoomOut => 'Zoom ut';
@override String get zoomReset => 'Tilbakestill zoom';
@override String get subSeekNext => 'Spol til neste undertekst'; @override String get subSeekNext => 'Spol til neste undertekst';
@override String get subSeekPrev => 'Spol til forrige undertekst'; @override String get subSeekPrev => 'Spol til forrige undertekst';
@override String get shaderToggle => 'Veksle shadere'; @override String get shaderToggle => 'Veksle shadere';
@@ -2099,6 +2104,9 @@ extension on TranslationsNb {
'hotkeys.actions.speedIncrease' => 'Øk hastighet', 'hotkeys.actions.speedIncrease' => 'Øk hastighet',
'hotkeys.actions.speedDecrease' => 'Reduser hastighet', 'hotkeys.actions.speedDecrease' => 'Reduser hastighet',
'hotkeys.actions.speedReset' => 'Tilbakestill hastighet', 'hotkeys.actions.speedReset' => 'Tilbakestill hastighet',
'hotkeys.actions.zoomIn' => 'Zoom inn',
'hotkeys.actions.zoomOut' => 'Zoom ut',
'hotkeys.actions.zoomReset' => 'Tilbakestill zoom',
'hotkeys.actions.subSeekNext' => 'Spol til neste undertekst', 'hotkeys.actions.subSeekNext' => 'Spol til neste undertekst',
'hotkeys.actions.subSeekPrev' => 'Spol til forrige undertekst', 'hotkeys.actions.subSeekPrev' => 'Spol til forrige undertekst',
'hotkeys.actions.shaderToggle' => 'Veksle shadere', 'hotkeys.actions.shaderToggle' => 'Veksle shadere',
@@ -2224,6 +2232,7 @@ extension on TranslationsNb {
'videoControls.pipActive' => 'Spiller i bilde-i-bilde', 'videoControls.pipActive' => 'Spiller i bilde-i-bilde',
'videoControls.pipFailed' => 'Bilde-i-bilde kunne ikke starte', 'videoControls.pipFailed' => 'Bilde-i-bilde kunne ikke starte',
'videoControls.screenshotSaved' => 'Skjermbilde lagret', 'videoControls.screenshotSaved' => 'Skjermbilde lagret',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Krever Android 8.0 eller nyere', 'videoControls.pipErrors.androidVersion' => 'Krever Android 8.0 eller nyere',
'videoControls.pipErrors.iosVersion' => 'Krever iOS 15.0 eller nyere', 'videoControls.pipErrors.iosVersion' => 'Krever iOS 15.0 eller nyere',
'videoControls.pipErrors.permissionDisabled' => 'Bilde-i-bilde er deaktivert. Slå det på i systeminnstillinger.', 'videoControls.pipErrors.permissionDisabled' => 'Bilde-i-bilde er deaktivert. Slå det på i systeminnstillinger.',
@@ -2316,12 +2325,12 @@ extension on TranslationsNb {
'profiles.addPlezyProfile' => 'Legg til Plezy-profil', 'profiles.addPlezyProfile' => 'Legg til Plezy-profil',
'profiles.switchingProfile' => 'Bytter profil…', 'profiles.switchingProfile' => 'Bytter profil…',
'profiles.deleteThisProfileTitle' => 'Slett denne profilen?', 'profiles.deleteThisProfileTitle' => 'Slett denne profilen?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Fjern ${displayName}. Tilkoblinger påvirkes ikke.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Fjern ${displayName}. Tilkoblinger påvirkes ikke.',
'profiles.active' => 'Aktiv', 'profiles.active' => 'Aktiv',
'profiles.manage' => 'Administrer', 'profiles.manage' => 'Administrer',
'profiles.delete' => 'Slett', 'profiles.delete' => 'Slett',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Logg ut', 'profiles.signOut' => 'Logg ut',
'profiles.signOutPlexTitle' => 'Logge ut av Plex?', 'profiles.signOutPlexTitle' => 'Logge ut av Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Fjerne ${displayName} og alle Plex Home-brukere? Du kan logge inn igjen når som helst.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Fjerne ${displayName} og alle Plex Home-brukere? Du kan logge inn igjen når som helst.',
@@ -2801,6 +2810,7 @@ extension on TranslationsNb {
'companionRemote.remote.audio' => 'Lyd', 'companionRemote.remote.audio' => 'Lyd',
'companionRemote.remote.searchHint' => 'Søk på stasjonær...', 'companionRemote.remote.searchHint' => 'Søk på stasjonær...',
'videoSettings.playbackSpeed' => 'Avspillingshastighet', 'videoSettings.playbackSpeed' => 'Avspillingshastighet',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Søvntimer', 'videoSettings.sleepTimer' => 'Søvntimer',
'videoSettings.audioSync' => 'Lydsynkronisering', 'videoSettings.audioSync' => 'Lydsynkronisering',
'videoSettings.subtitleSync' => 'Undertekstsynkronisering', 'videoSettings.subtitleSync' => 'Undertekstsynkronisering',
@@ -2829,13 +2839,13 @@ extension on TranslationsNb {
'metadataEdit.basicInfo' => 'Grunnleggende info', 'metadataEdit.basicInfo' => 'Grunnleggende info',
'metadataEdit.artwork' => 'Kunstverk', 'metadataEdit.artwork' => 'Kunstverk',
'metadataEdit.advancedSettings' => 'Avanserte innstillinger', 'metadataEdit.advancedSettings' => 'Avanserte innstillinger',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Tittel', 'metadataEdit.title' => 'Tittel',
'metadataEdit.sortTitle' => 'Sorteringsstittel', 'metadataEdit.sortTitle' => 'Sorteringsstittel',
'metadataEdit.originalTitle' => 'Originaltittel', 'metadataEdit.originalTitle' => 'Originaltittel',
'metadataEdit.releaseDate' => 'Utgivelsesdato', 'metadataEdit.releaseDate' => 'Utgivelsesdato',
'metadataEdit.contentRating' => 'Innholdsvurdering', 'metadataEdit.contentRating' => 'Innholdsvurdering',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studio', 'metadataEdit.studio' => 'Studio',
'metadataEdit.tagline' => 'Slagord', 'metadataEdit.tagline' => 'Slagord',
'metadataEdit.summary' => 'Sammendrag', 'metadataEdit.summary' => 'Sammendrag',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsNl extends TranslationsVideoControlsEn {
@override String get pipActive => 'Afspelen in beeld-in-beeld'; @override String get pipActive => 'Afspelen in beeld-in-beeld';
@override String get pipFailed => 'Beeld-in-beeld kon niet worden gestart'; @override String get pipFailed => 'Beeld-in-beeld kon niet worden gestart';
@override String get screenshotSaved => 'Schermafbeelding opgeslagen'; @override String get screenshotSaved => 'Schermafbeelding opgeslagen';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsNl pipErrors = _TranslationsVideoControlsPipErrorsNl._(_root); @override late final _TranslationsVideoControlsPipErrorsNl pipErrors = _TranslationsVideoControlsPipErrorsNl._(_root);
@override String get chapters => 'Hoofdstukken'; @override String get chapters => 'Hoofdstukken';
@override String get noChaptersAvailable => 'Geen hoofdstukken beschikbaar'; @override String get noChaptersAvailable => 'Geen hoofdstukken beschikbaar';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsNl extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Afspeelsnelheid'; @override String get playbackSpeed => 'Afspeelsnelheid';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Slaaptimer'; @override String get sleepTimer => 'Slaaptimer';
@override String get audioSync => 'Audio synchronisatie'; @override String get audioSync => 'Audio synchronisatie';
@override String get subtitleSync => 'Ondertitel synchronisatie'; @override String get subtitleSync => 'Ondertitel synchronisatie';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsNl extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Snelheid verhogen'; @override String get speedIncrease => 'Snelheid verhogen';
@override String get speedDecrease => 'Snelheid verlagen'; @override String get speedDecrease => 'Snelheid verlagen';
@override String get speedReset => 'Snelheid resetten'; @override String get speedReset => 'Snelheid resetten';
@override String get zoomIn => 'Inzoomen';
@override String get zoomOut => 'Uitzoomen';
@override String get zoomReset => 'Zoom resetten';
@override String get subSeekNext => 'Naar volgende ondertitel'; @override String get subSeekNext => 'Naar volgende ondertitel';
@override String get subSeekPrev => 'Naar vorige ondertitel'; @override String get subSeekPrev => 'Naar vorige ondertitel';
@override String get shaderToggle => 'Shaders aan/uit'; @override String get shaderToggle => 'Shaders aan/uit';
@@ -2099,6 +2104,9 @@ extension on TranslationsNl {
'hotkeys.actions.speedIncrease' => 'Snelheid verhogen', 'hotkeys.actions.speedIncrease' => 'Snelheid verhogen',
'hotkeys.actions.speedDecrease' => 'Snelheid verlagen', 'hotkeys.actions.speedDecrease' => 'Snelheid verlagen',
'hotkeys.actions.speedReset' => 'Snelheid resetten', 'hotkeys.actions.speedReset' => 'Snelheid resetten',
'hotkeys.actions.zoomIn' => 'Inzoomen',
'hotkeys.actions.zoomOut' => 'Uitzoomen',
'hotkeys.actions.zoomReset' => 'Zoom resetten',
'hotkeys.actions.subSeekNext' => 'Naar volgende ondertitel', 'hotkeys.actions.subSeekNext' => 'Naar volgende ondertitel',
'hotkeys.actions.subSeekPrev' => 'Naar vorige ondertitel', 'hotkeys.actions.subSeekPrev' => 'Naar vorige ondertitel',
'hotkeys.actions.shaderToggle' => 'Shaders aan/uit', 'hotkeys.actions.shaderToggle' => 'Shaders aan/uit',
@@ -2224,6 +2232,7 @@ extension on TranslationsNl {
'videoControls.pipActive' => 'Afspelen in beeld-in-beeld', 'videoControls.pipActive' => 'Afspelen in beeld-in-beeld',
'videoControls.pipFailed' => 'Beeld-in-beeld kon niet worden gestart', 'videoControls.pipFailed' => 'Beeld-in-beeld kon niet worden gestart',
'videoControls.screenshotSaved' => 'Schermafbeelding opgeslagen', 'videoControls.screenshotSaved' => 'Schermafbeelding opgeslagen',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Vereist Android 8.0 of nieuwer', 'videoControls.pipErrors.androidVersion' => 'Vereist Android 8.0 of nieuwer',
'videoControls.pipErrors.iosVersion' => 'Vereist iOS 15.0 of nieuwer', 'videoControls.pipErrors.iosVersion' => 'Vereist iOS 15.0 of nieuwer',
'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture is uitgeschakeld. Schakel het in via systeeminstellingen.', 'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture is uitgeschakeld. Schakel het in via systeeminstellingen.',
@@ -2316,12 +2325,12 @@ extension on TranslationsNl {
'profiles.addPlezyProfile' => 'Plezy-profiel toevoegen', 'profiles.addPlezyProfile' => 'Plezy-profiel toevoegen',
'profiles.switchingProfile' => 'Profiel wisselen…', 'profiles.switchingProfile' => 'Profiel wisselen…',
'profiles.deleteThisProfileTitle' => 'Dit profiel verwijderen?', 'profiles.deleteThisProfileTitle' => 'Dit profiel verwijderen?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Verwijder ${displayName}. Verbindingen blijven ongewijzigd.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Verwijder ${displayName}. Verbindingen blijven ongewijzigd.',
'profiles.active' => 'Actief', 'profiles.active' => 'Actief',
'profiles.manage' => 'Beheren', 'profiles.manage' => 'Beheren',
'profiles.delete' => 'Verwijderen', 'profiles.delete' => 'Verwijderen',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Afmelden', 'profiles.signOut' => 'Afmelden',
'profiles.signOutPlexTitle' => 'Afmelden bij Plex?', 'profiles.signOutPlexTitle' => 'Afmelden bij Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} en alle Plex Home-gebruikers verwijderen? Je kunt altijd opnieuw inloggen.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} en alle Plex Home-gebruikers verwijderen? Je kunt altijd opnieuw inloggen.',
@@ -2801,6 +2810,7 @@ extension on TranslationsNl {
'companionRemote.remote.audio' => 'Audio', 'companionRemote.remote.audio' => 'Audio',
'companionRemote.remote.searchHint' => 'Zoeken op desktop...', 'companionRemote.remote.searchHint' => 'Zoeken op desktop...',
'videoSettings.playbackSpeed' => 'Afspeelsnelheid', 'videoSettings.playbackSpeed' => 'Afspeelsnelheid',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Slaaptimer', 'videoSettings.sleepTimer' => 'Slaaptimer',
'videoSettings.audioSync' => 'Audio synchronisatie', 'videoSettings.audioSync' => 'Audio synchronisatie',
'videoSettings.subtitleSync' => 'Ondertitel synchronisatie', 'videoSettings.subtitleSync' => 'Ondertitel synchronisatie',
@@ -2829,13 +2839,13 @@ extension on TranslationsNl {
'metadataEdit.basicInfo' => 'Basisinformatie', 'metadataEdit.basicInfo' => 'Basisinformatie',
'metadataEdit.artwork' => 'Artwork', 'metadataEdit.artwork' => 'Artwork',
'metadataEdit.advancedSettings' => 'Geavanceerde instellingen', 'metadataEdit.advancedSettings' => 'Geavanceerde instellingen',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Titel', 'metadataEdit.title' => 'Titel',
'metadataEdit.sortTitle' => 'Sorteertitel', 'metadataEdit.sortTitle' => 'Sorteertitel',
'metadataEdit.originalTitle' => 'Oorspronkelijke titel', 'metadataEdit.originalTitle' => 'Oorspronkelijke titel',
'metadataEdit.releaseDate' => 'Releasedatum', 'metadataEdit.releaseDate' => 'Releasedatum',
'metadataEdit.contentRating' => 'Leeftijdsclassificatie', 'metadataEdit.contentRating' => 'Leeftijdsclassificatie',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studio', 'metadataEdit.studio' => 'Studio',
'metadataEdit.tagline' => 'Tagline', 'metadataEdit.tagline' => 'Tagline',
'metadataEdit.summary' => 'Samenvatting', 'metadataEdit.summary' => 'Samenvatting',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsPl extends TranslationsVideoControlsEn {
@override String get pipActive => 'Odtwarzanie w trybie obraz w obrazie'; @override String get pipActive => 'Odtwarzanie w trybie obraz w obrazie';
@override String get pipFailed => 'Nie udało się uruchomić trybu obraz w obrazie'; @override String get pipFailed => 'Nie udało się uruchomić trybu obraz w obrazie';
@override String get screenshotSaved => 'Zrzut ekranu zapisany'; @override String get screenshotSaved => 'Zrzut ekranu zapisany';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsPl pipErrors = _TranslationsVideoControlsPipErrorsPl._(_root); @override late final _TranslationsVideoControlsPipErrorsPl pipErrors = _TranslationsVideoControlsPipErrorsPl._(_root);
@override String get chapters => 'Rozdziały'; @override String get chapters => 'Rozdziały';
@override String get noChaptersAvailable => 'Brak dostępnych rozdziałów'; @override String get noChaptersAvailable => 'Brak dostępnych rozdziałów';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsPl extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Prędkość odtwarzania'; @override String get playbackSpeed => 'Prędkość odtwarzania';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Wyłącznik czasowy'; @override String get sleepTimer => 'Wyłącznik czasowy';
@override String get audioSync => 'Synchronizacja audio'; @override String get audioSync => 'Synchronizacja audio';
@override String get subtitleSync => 'Synchronizacja napisów'; @override String get subtitleSync => 'Synchronizacja napisów';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsPl extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Zwiększ prędkość'; @override String get speedIncrease => 'Zwiększ prędkość';
@override String get speedDecrease => 'Zmniejsz prędkość'; @override String get speedDecrease => 'Zmniejsz prędkość';
@override String get speedReset => 'Zresetuj prędkość'; @override String get speedReset => 'Zresetuj prędkość';
@override String get zoomIn => 'Powiększ';
@override String get zoomOut => 'Pomniejsz';
@override String get zoomReset => 'Zresetuj zoom';
@override String get subSeekNext => 'Przewiń do następnego napisu'; @override String get subSeekNext => 'Przewiń do następnego napisu';
@override String get subSeekPrev => 'Przewiń do poprzedniego napisu'; @override String get subSeekPrev => 'Przewiń do poprzedniego napisu';
@override String get shaderToggle => 'Przełącz shadery'; @override String get shaderToggle => 'Przełącz shadery';
@@ -2099,6 +2104,9 @@ extension on TranslationsPl {
'hotkeys.actions.speedIncrease' => 'Zwiększ prędkość', 'hotkeys.actions.speedIncrease' => 'Zwiększ prędkość',
'hotkeys.actions.speedDecrease' => 'Zmniejsz prędkość', 'hotkeys.actions.speedDecrease' => 'Zmniejsz prędkość',
'hotkeys.actions.speedReset' => 'Zresetuj prędkość', 'hotkeys.actions.speedReset' => 'Zresetuj prędkość',
'hotkeys.actions.zoomIn' => 'Powiększ',
'hotkeys.actions.zoomOut' => 'Pomniejsz',
'hotkeys.actions.zoomReset' => 'Zresetuj zoom',
'hotkeys.actions.subSeekNext' => 'Przewiń do następnego napisu', 'hotkeys.actions.subSeekNext' => 'Przewiń do następnego napisu',
'hotkeys.actions.subSeekPrev' => 'Przewiń do poprzedniego napisu', 'hotkeys.actions.subSeekPrev' => 'Przewiń do poprzedniego napisu',
'hotkeys.actions.shaderToggle' => 'Przełącz shadery', 'hotkeys.actions.shaderToggle' => 'Przełącz shadery',
@@ -2224,6 +2232,7 @@ extension on TranslationsPl {
'videoControls.pipActive' => 'Odtwarzanie w trybie obraz w obrazie', 'videoControls.pipActive' => 'Odtwarzanie w trybie obraz w obrazie',
'videoControls.pipFailed' => 'Nie udało się uruchomić trybu obraz w obrazie', 'videoControls.pipFailed' => 'Nie udało się uruchomić trybu obraz w obrazie',
'videoControls.screenshotSaved' => 'Zrzut ekranu zapisany', 'videoControls.screenshotSaved' => 'Zrzut ekranu zapisany',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Wymaga Androida 8.0 lub nowszego', 'videoControls.pipErrors.androidVersion' => 'Wymaga Androida 8.0 lub nowszego',
'videoControls.pipErrors.iosVersion' => 'Wymaga iOS 15.0 lub nowszego', 'videoControls.pipErrors.iosVersion' => 'Wymaga iOS 15.0 lub nowszego',
'videoControls.pipErrors.permissionDisabled' => 'Obraz w obrazie jest wyłączony. Włącz go w ustawieniach systemu.', 'videoControls.pipErrors.permissionDisabled' => 'Obraz w obrazie jest wyłączony. Włącz go w ustawieniach systemu.',
@@ -2316,12 +2325,12 @@ extension on TranslationsPl {
'profiles.addPlezyProfile' => 'Dodaj profil Plezy', 'profiles.addPlezyProfile' => 'Dodaj profil Plezy',
'profiles.switchingProfile' => 'Przełączanie profilu…', 'profiles.switchingProfile' => 'Przełączanie profilu…',
'profiles.deleteThisProfileTitle' => 'Usunąć ten profil?', 'profiles.deleteThisProfileTitle' => 'Usunąć ten profil?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Usuń ${displayName}. Połączenia nie zostaną zmienione.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Usuń ${displayName}. Połączenia nie zostaną zmienione.',
'profiles.active' => 'Aktywny', 'profiles.active' => 'Aktywny',
'profiles.manage' => 'Zarządzaj', 'profiles.manage' => 'Zarządzaj',
'profiles.delete' => 'Usuń', 'profiles.delete' => 'Usuń',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Wyloguj się', 'profiles.signOut' => 'Wyloguj się',
'profiles.signOutPlexTitle' => 'Wylogować się z Plex?', 'profiles.signOutPlexTitle' => 'Wylogować się z Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Usunąć ${displayName} i wszystkich użytkowników Plex Home? Możesz zalogować się ponownie w każdej chwili.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Usunąć ${displayName} i wszystkich użytkowników Plex Home? Możesz zalogować się ponownie w każdej chwili.',
@@ -2801,6 +2810,7 @@ extension on TranslationsPl {
'companionRemote.remote.audio' => 'Audio', 'companionRemote.remote.audio' => 'Audio',
'companionRemote.remote.searchHint' => 'Szukaj na komputerze...', 'companionRemote.remote.searchHint' => 'Szukaj na komputerze...',
'videoSettings.playbackSpeed' => 'Prędkość odtwarzania', 'videoSettings.playbackSpeed' => 'Prędkość odtwarzania',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Wyłącznik czasowy', 'videoSettings.sleepTimer' => 'Wyłącznik czasowy',
'videoSettings.audioSync' => 'Synchronizacja audio', 'videoSettings.audioSync' => 'Synchronizacja audio',
'videoSettings.subtitleSync' => 'Synchronizacja napisów', 'videoSettings.subtitleSync' => 'Synchronizacja napisów',
@@ -2829,13 +2839,13 @@ extension on TranslationsPl {
'metadataEdit.basicInfo' => 'Podstawowe informacje', 'metadataEdit.basicInfo' => 'Podstawowe informacje',
'metadataEdit.artwork' => 'Grafika', 'metadataEdit.artwork' => 'Grafika',
'metadataEdit.advancedSettings' => 'Ustawienia zaawansowane', 'metadataEdit.advancedSettings' => 'Ustawienia zaawansowane',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Tytuł', 'metadataEdit.title' => 'Tytuł',
'metadataEdit.sortTitle' => 'Tytuł do sortowania', 'metadataEdit.sortTitle' => 'Tytuł do sortowania',
'metadataEdit.originalTitle' => 'Tytuł oryginalny', 'metadataEdit.originalTitle' => 'Tytuł oryginalny',
'metadataEdit.releaseDate' => 'Data premiery', 'metadataEdit.releaseDate' => 'Data premiery',
'metadataEdit.contentRating' => 'Klasyfikacja wiekowa', 'metadataEdit.contentRating' => 'Klasyfikacja wiekowa',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studio', 'metadataEdit.studio' => 'Studio',
'metadataEdit.tagline' => 'Tagline', 'metadataEdit.tagline' => 'Tagline',
'metadataEdit.summary' => 'Opis', 'metadataEdit.summary' => 'Opis',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsPt extends TranslationsVideoControlsEn {
@override String get pipActive => 'Reproduzindo em Picture-in-Picture'; @override String get pipActive => 'Reproduzindo em Picture-in-Picture';
@override String get pipFailed => 'Falha ao iniciar picture-in-picture'; @override String get pipFailed => 'Falha ao iniciar picture-in-picture';
@override String get screenshotSaved => 'Captura de tela salva'; @override String get screenshotSaved => 'Captura de tela salva';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsPt pipErrors = _TranslationsVideoControlsPipErrorsPt._(_root); @override late final _TranslationsVideoControlsPipErrorsPt pipErrors = _TranslationsVideoControlsPipErrorsPt._(_root);
@override String get chapters => 'Capítulos'; @override String get chapters => 'Capítulos';
@override String get noChaptersAvailable => 'Nenhum capítulo disponível'; @override String get noChaptersAvailable => 'Nenhum capítulo disponível';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsPt extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Velocidade de Reprodução'; @override String get playbackSpeed => 'Velocidade de Reprodução';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Timer de Sono'; @override String get sleepTimer => 'Timer de Sono';
@override String get audioSync => 'Sincronia de Áudio'; @override String get audioSync => 'Sincronia de Áudio';
@override String get subtitleSync => 'Sincronia de Legendas'; @override String get subtitleSync => 'Sincronia de Legendas';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsPt extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Aumentar Velocidade'; @override String get speedIncrease => 'Aumentar Velocidade';
@override String get speedDecrease => 'Diminuir Velocidade'; @override String get speedDecrease => 'Diminuir Velocidade';
@override String get speedReset => 'Redefinir Velocidade'; @override String get speedReset => 'Redefinir Velocidade';
@override String get zoomIn => 'Aumentar zoom';
@override String get zoomOut => 'Diminuir zoom';
@override String get zoomReset => 'Redefinir zoom';
@override String get subSeekNext => 'Ir para Próxima Legenda'; @override String get subSeekNext => 'Ir para Próxima Legenda';
@override String get subSeekPrev => 'Ir para Legenda Anterior'; @override String get subSeekPrev => 'Ir para Legenda Anterior';
@override String get shaderToggle => 'Alternar Shaders'; @override String get shaderToggle => 'Alternar Shaders';
@@ -2099,6 +2104,9 @@ extension on TranslationsPt {
'hotkeys.actions.speedIncrease' => 'Aumentar Velocidade', 'hotkeys.actions.speedIncrease' => 'Aumentar Velocidade',
'hotkeys.actions.speedDecrease' => 'Diminuir Velocidade', 'hotkeys.actions.speedDecrease' => 'Diminuir Velocidade',
'hotkeys.actions.speedReset' => 'Redefinir Velocidade', 'hotkeys.actions.speedReset' => 'Redefinir Velocidade',
'hotkeys.actions.zoomIn' => 'Aumentar zoom',
'hotkeys.actions.zoomOut' => 'Diminuir zoom',
'hotkeys.actions.zoomReset' => 'Redefinir zoom',
'hotkeys.actions.subSeekNext' => 'Ir para Próxima Legenda', 'hotkeys.actions.subSeekNext' => 'Ir para Próxima Legenda',
'hotkeys.actions.subSeekPrev' => 'Ir para Legenda Anterior', 'hotkeys.actions.subSeekPrev' => 'Ir para Legenda Anterior',
'hotkeys.actions.shaderToggle' => 'Alternar Shaders', 'hotkeys.actions.shaderToggle' => 'Alternar Shaders',
@@ -2224,6 +2232,7 @@ extension on TranslationsPt {
'videoControls.pipActive' => 'Reproduzindo em Picture-in-Picture', 'videoControls.pipActive' => 'Reproduzindo em Picture-in-Picture',
'videoControls.pipFailed' => 'Falha ao iniciar picture-in-picture', 'videoControls.pipFailed' => 'Falha ao iniciar picture-in-picture',
'videoControls.screenshotSaved' => 'Captura de tela salva', 'videoControls.screenshotSaved' => 'Captura de tela salva',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Requer Android 8.0 ou superior', 'videoControls.pipErrors.androidVersion' => 'Requer Android 8.0 ou superior',
'videoControls.pipErrors.iosVersion' => 'Requer iOS 15.0 ou superior', 'videoControls.pipErrors.iosVersion' => 'Requer iOS 15.0 ou superior',
'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture está desativado. Ative nas configurações do sistema.', 'videoControls.pipErrors.permissionDisabled' => 'Picture-in-picture está desativado. Ative nas configurações do sistema.',
@@ -2316,12 +2325,12 @@ extension on TranslationsPt {
'profiles.addPlezyProfile' => 'Adicionar perfil Plezy', 'profiles.addPlezyProfile' => 'Adicionar perfil Plezy',
'profiles.switchingProfile' => 'Mudando perfil…', 'profiles.switchingProfile' => 'Mudando perfil…',
'profiles.deleteThisProfileTitle' => 'Excluir este perfil?', 'profiles.deleteThisProfileTitle' => 'Excluir este perfil?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Remover ${displayName}. As conexões não serão afetadas.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Remover ${displayName}. As conexões não serão afetadas.',
'profiles.active' => 'Ativo', 'profiles.active' => 'Ativo',
'profiles.manage' => 'Gerenciar', 'profiles.manage' => 'Gerenciar',
'profiles.delete' => 'Excluir', 'profiles.delete' => 'Excluir',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Sair', 'profiles.signOut' => 'Sair',
'profiles.signOutPlexTitle' => 'Sair do Plex?', 'profiles.signOutPlexTitle' => 'Sair do Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Remover ${displayName} e todos os usuários Plex Home? Você pode entrar novamente quando quiser.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Remover ${displayName} e todos os usuários Plex Home? Você pode entrar novamente quando quiser.',
@@ -2801,6 +2810,7 @@ extension on TranslationsPt {
'companionRemote.remote.audio' => 'Áudio', 'companionRemote.remote.audio' => 'Áudio',
'companionRemote.remote.searchHint' => 'Buscar no desktop...', 'companionRemote.remote.searchHint' => 'Buscar no desktop...',
'videoSettings.playbackSpeed' => 'Velocidade de Reprodução', 'videoSettings.playbackSpeed' => 'Velocidade de Reprodução',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Timer de Sono', 'videoSettings.sleepTimer' => 'Timer de Sono',
'videoSettings.audioSync' => 'Sincronia de Áudio', 'videoSettings.audioSync' => 'Sincronia de Áudio',
'videoSettings.subtitleSync' => 'Sincronia de Legendas', 'videoSettings.subtitleSync' => 'Sincronia de Legendas',
@@ -2829,13 +2839,13 @@ extension on TranslationsPt {
'metadataEdit.basicInfo' => 'Informações Básicas', 'metadataEdit.basicInfo' => 'Informações Básicas',
'metadataEdit.artwork' => 'Arte', 'metadataEdit.artwork' => 'Arte',
'metadataEdit.advancedSettings' => 'Configurações Avançadas', 'metadataEdit.advancedSettings' => 'Configurações Avançadas',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Título', 'metadataEdit.title' => 'Título',
'metadataEdit.sortTitle' => 'Título para Ordenação', 'metadataEdit.sortTitle' => 'Título para Ordenação',
'metadataEdit.originalTitle' => 'Título Original', 'metadataEdit.originalTitle' => 'Título Original',
'metadataEdit.releaseDate' => 'Data de Lançamento', 'metadataEdit.releaseDate' => 'Data de Lançamento',
'metadataEdit.contentRating' => 'Classificação Indicativa', 'metadataEdit.contentRating' => 'Classificação Indicativa',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Estúdio', 'metadataEdit.studio' => 'Estúdio',
'metadataEdit.tagline' => 'Tagline', 'metadataEdit.tagline' => 'Tagline',
'metadataEdit.summary' => 'Sinopse', 'metadataEdit.summary' => 'Sinopse',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsRu extends TranslationsVideoControlsEn {
@override String get pipActive => 'Воспроизводится в режиме «картинка в картинке»'; @override String get pipActive => 'Воспроизводится в режиме «картинка в картинке»';
@override String get pipFailed => 'Не удалось запустить режим «картинка в картинке»'; @override String get pipFailed => 'Не удалось запустить режим «картинка в картинке»';
@override String get screenshotSaved => 'Снимок экрана сохранён'; @override String get screenshotSaved => 'Снимок экрана сохранён';
@override String zoomPercent({required Object percent}) => 'Масштаб ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsRu pipErrors = _TranslationsVideoControlsPipErrorsRu._(_root); @override late final _TranslationsVideoControlsPipErrorsRu pipErrors = _TranslationsVideoControlsPipErrorsRu._(_root);
@override String get chapters => 'Главы'; @override String get chapters => 'Главы';
@override String get noChaptersAvailable => 'Главы недоступны'; @override String get noChaptersAvailable => 'Главы недоступны';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsRu extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Скорость воспроизведения'; @override String get playbackSpeed => 'Скорость воспроизведения';
@override String get zoom => 'Масштаб';
@override String get sleepTimer => 'Таймер сна'; @override String get sleepTimer => 'Таймер сна';
@override String get audioSync => 'Синхронизация аудио'; @override String get audioSync => 'Синхронизация аудио';
@override String get subtitleSync => 'Синхронизация субтитров'; @override String get subtitleSync => 'Синхронизация субтитров';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsRu extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Увеличить скорость'; @override String get speedIncrease => 'Увеличить скорость';
@override String get speedDecrease => 'Уменьшить скорость'; @override String get speedDecrease => 'Уменьшить скорость';
@override String get speedReset => 'Сбросить скорость'; @override String get speedReset => 'Сбросить скорость';
@override String get zoomIn => 'Увеличить масштаб';
@override String get zoomOut => 'Уменьшить масштаб';
@override String get zoomReset => 'Сбросить масштаб';
@override String get subSeekNext => 'К следующему субтитру'; @override String get subSeekNext => 'К следующему субтитру';
@override String get subSeekPrev => 'К предыдущему субтитру'; @override String get subSeekPrev => 'К предыдущему субтитру';
@override String get shaderToggle => 'Вкл./выкл. шейдеры'; @override String get shaderToggle => 'Вкл./выкл. шейдеры';
@@ -2099,6 +2104,9 @@ extension on TranslationsRu {
'hotkeys.actions.speedIncrease' => 'Увеличить скорость', 'hotkeys.actions.speedIncrease' => 'Увеличить скорость',
'hotkeys.actions.speedDecrease' => 'Уменьшить скорость', 'hotkeys.actions.speedDecrease' => 'Уменьшить скорость',
'hotkeys.actions.speedReset' => 'Сбросить скорость', 'hotkeys.actions.speedReset' => 'Сбросить скорость',
'hotkeys.actions.zoomIn' => 'Увеличить масштаб',
'hotkeys.actions.zoomOut' => 'Уменьшить масштаб',
'hotkeys.actions.zoomReset' => 'Сбросить масштаб',
'hotkeys.actions.subSeekNext' => 'К следующему субтитру', 'hotkeys.actions.subSeekNext' => 'К следующему субтитру',
'hotkeys.actions.subSeekPrev' => 'К предыдущему субтитру', 'hotkeys.actions.subSeekPrev' => 'К предыдущему субтитру',
'hotkeys.actions.shaderToggle' => 'Вкл./выкл. шейдеры', 'hotkeys.actions.shaderToggle' => 'Вкл./выкл. шейдеры',
@@ -2224,6 +2232,7 @@ extension on TranslationsRu {
'videoControls.pipActive' => 'Воспроизводится в режиме «картинка в картинке»', 'videoControls.pipActive' => 'Воспроизводится в режиме «картинка в картинке»',
'videoControls.pipFailed' => 'Не удалось запустить режим «картинка в картинке»', 'videoControls.pipFailed' => 'Не удалось запустить режим «картинка в картинке»',
'videoControls.screenshotSaved' => 'Снимок экрана сохранён', 'videoControls.screenshotSaved' => 'Снимок экрана сохранён',
'videoControls.zoomPercent' => ({required Object percent}) => 'Масштаб ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Требуется Android 8.0 или новее', 'videoControls.pipErrors.androidVersion' => 'Требуется Android 8.0 или новее',
'videoControls.pipErrors.iosVersion' => 'Требуется iOS 15.0 или новее', 'videoControls.pipErrors.iosVersion' => 'Требуется iOS 15.0 или новее',
'videoControls.pipErrors.permissionDisabled' => 'Картинка-в-картинке отключена. Включите ее в системных настройках.', 'videoControls.pipErrors.permissionDisabled' => 'Картинка-в-картинке отключена. Включите ее в системных настройках.',
@@ -2316,12 +2325,12 @@ extension on TranslationsRu {
'profiles.addPlezyProfile' => 'Добавить профиль Plezy', 'profiles.addPlezyProfile' => 'Добавить профиль Plezy',
'profiles.switchingProfile' => 'Переключение профиля…', 'profiles.switchingProfile' => 'Переключение профиля…',
'profiles.deleteThisProfileTitle' => 'Удалить этот профиль?', 'profiles.deleteThisProfileTitle' => 'Удалить этот профиль?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Удалить ${displayName}. Подключения не изменятся.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Удалить ${displayName}. Подключения не изменятся.',
'profiles.active' => 'Активный', 'profiles.active' => 'Активный',
'profiles.manage' => 'Управление', 'profiles.manage' => 'Управление',
'profiles.delete' => 'Удалить', 'profiles.delete' => 'Удалить',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Выйти', 'profiles.signOut' => 'Выйти',
'profiles.signOutPlexTitle' => 'Выйти из Plex?', 'profiles.signOutPlexTitle' => 'Выйти из Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Удалить ${displayName} и всех пользователей Plex Home? Вы сможете войти снова в любое время.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Удалить ${displayName} и всех пользователей Plex Home? Вы сможете войти снова в любое время.',
@@ -2801,6 +2810,7 @@ extension on TranslationsRu {
'companionRemote.remote.audio' => 'Аудио', 'companionRemote.remote.audio' => 'Аудио',
'companionRemote.remote.searchHint' => 'Поиск на десктопе...', 'companionRemote.remote.searchHint' => 'Поиск на десктопе...',
'videoSettings.playbackSpeed' => 'Скорость воспроизведения', 'videoSettings.playbackSpeed' => 'Скорость воспроизведения',
'videoSettings.zoom' => 'Масштаб',
'videoSettings.sleepTimer' => 'Таймер сна', 'videoSettings.sleepTimer' => 'Таймер сна',
'videoSettings.audioSync' => 'Синхронизация аудио', 'videoSettings.audioSync' => 'Синхронизация аудио',
'videoSettings.subtitleSync' => 'Синхронизация субтитров', 'videoSettings.subtitleSync' => 'Синхронизация субтитров',
@@ -2829,13 +2839,13 @@ extension on TranslationsRu {
'metadataEdit.basicInfo' => 'Основная информация', 'metadataEdit.basicInfo' => 'Основная информация',
'metadataEdit.artwork' => 'Обложка', 'metadataEdit.artwork' => 'Обложка',
'metadataEdit.advancedSettings' => 'Дополнительные настройки', 'metadataEdit.advancedSettings' => 'Дополнительные настройки',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Название', 'metadataEdit.title' => 'Название',
'metadataEdit.sortTitle' => 'Название для сортировки', 'metadataEdit.sortTitle' => 'Название для сортировки',
'metadataEdit.originalTitle' => 'Оригинальное название', 'metadataEdit.originalTitle' => 'Оригинальное название',
'metadataEdit.releaseDate' => 'Дата выпуска', 'metadataEdit.releaseDate' => 'Дата выпуска',
'metadataEdit.contentRating' => 'Возрастной рейтинг', 'metadataEdit.contentRating' => 'Возрастной рейтинг',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Студия', 'metadataEdit.studio' => 'Студия',
'metadataEdit.tagline' => 'Слоган', 'metadataEdit.tagline' => 'Слоган',
'metadataEdit.summary' => 'Описание', 'metadataEdit.summary' => 'Описание',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsSv extends TranslationsVideoControlsEn {
@override String get pipActive => 'Spelar i bild-i-bild'; @override String get pipActive => 'Spelar i bild-i-bild';
@override String get pipFailed => 'Bild-i-bild kunde inte starta'; @override String get pipFailed => 'Bild-i-bild kunde inte starta';
@override String get screenshotSaved => 'Skärmbild sparad'; @override String get screenshotSaved => 'Skärmbild sparad';
@override String zoomPercent({required Object percent}) => 'Zoom ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsSv pipErrors = _TranslationsVideoControlsPipErrorsSv._(_root); @override late final _TranslationsVideoControlsPipErrorsSv pipErrors = _TranslationsVideoControlsPipErrorsSv._(_root);
@override String get chapters => 'Kapitel'; @override String get chapters => 'Kapitel';
@override String get noChaptersAvailable => 'Inga kapitel tillgängliga'; @override String get noChaptersAvailable => 'Inga kapitel tillgängliga';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsSv extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => 'Uppspelningshastighet'; @override String get playbackSpeed => 'Uppspelningshastighet';
@override String get zoom => 'Zoom';
@override String get sleepTimer => 'Sovtimer'; @override String get sleepTimer => 'Sovtimer';
@override String get audioSync => 'Ljudsynkronisering'; @override String get audioSync => 'Ljudsynkronisering';
@override String get subtitleSync => 'Undertextsynkronisering'; @override String get subtitleSync => 'Undertextsynkronisering';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsSv extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => 'Öka hastighet'; @override String get speedIncrease => 'Öka hastighet';
@override String get speedDecrease => 'Minska hastighet'; @override String get speedDecrease => 'Minska hastighet';
@override String get speedReset => 'Återställ hastighet'; @override String get speedReset => 'Återställ hastighet';
@override String get zoomIn => 'Zooma in';
@override String get zoomOut => 'Zooma ut';
@override String get zoomReset => 'Återställ zoom';
@override String get subSeekNext => 'Hoppa till nästa undertext'; @override String get subSeekNext => 'Hoppa till nästa undertext';
@override String get subSeekPrev => 'Hoppa till föregående undertext'; @override String get subSeekPrev => 'Hoppa till föregående undertext';
@override String get shaderToggle => 'Växla shaders'; @override String get shaderToggle => 'Växla shaders';
@@ -2099,6 +2104,9 @@ extension on TranslationsSv {
'hotkeys.actions.speedIncrease' => 'Öka hastighet', 'hotkeys.actions.speedIncrease' => 'Öka hastighet',
'hotkeys.actions.speedDecrease' => 'Minska hastighet', 'hotkeys.actions.speedDecrease' => 'Minska hastighet',
'hotkeys.actions.speedReset' => 'Återställ hastighet', 'hotkeys.actions.speedReset' => 'Återställ hastighet',
'hotkeys.actions.zoomIn' => 'Zooma in',
'hotkeys.actions.zoomOut' => 'Zooma ut',
'hotkeys.actions.zoomReset' => 'Återställ zoom',
'hotkeys.actions.subSeekNext' => 'Hoppa till nästa undertext', 'hotkeys.actions.subSeekNext' => 'Hoppa till nästa undertext',
'hotkeys.actions.subSeekPrev' => 'Hoppa till föregående undertext', 'hotkeys.actions.subSeekPrev' => 'Hoppa till föregående undertext',
'hotkeys.actions.shaderToggle' => 'Växla shaders', 'hotkeys.actions.shaderToggle' => 'Växla shaders',
@@ -2224,6 +2232,7 @@ extension on TranslationsSv {
'videoControls.pipActive' => 'Spelar i bild-i-bild', 'videoControls.pipActive' => 'Spelar i bild-i-bild',
'videoControls.pipFailed' => 'Bild-i-bild kunde inte starta', 'videoControls.pipFailed' => 'Bild-i-bild kunde inte starta',
'videoControls.screenshotSaved' => 'Skärmbild sparad', 'videoControls.screenshotSaved' => 'Skärmbild sparad',
'videoControls.zoomPercent' => ({required Object percent}) => 'Zoom ${percent}%',
'videoControls.pipErrors.androidVersion' => 'Kräver Android 8.0 eller nyare', 'videoControls.pipErrors.androidVersion' => 'Kräver Android 8.0 eller nyare',
'videoControls.pipErrors.iosVersion' => 'Kräver iOS 15.0 eller nyare', 'videoControls.pipErrors.iosVersion' => 'Kräver iOS 15.0 eller nyare',
'videoControls.pipErrors.permissionDisabled' => 'Bild-i-bild är inaktiverat. Aktivera det i systeminställningarna.', 'videoControls.pipErrors.permissionDisabled' => 'Bild-i-bild är inaktiverat. Aktivera det i systeminställningarna.',
@@ -2316,12 +2325,12 @@ extension on TranslationsSv {
'profiles.addPlezyProfile' => 'Lägg till Plezy-profil', 'profiles.addPlezyProfile' => 'Lägg till Plezy-profil',
'profiles.switchingProfile' => 'Byter profil…', 'profiles.switchingProfile' => 'Byter profil…',
'profiles.deleteThisProfileTitle' => 'Ta bort denna profil?', 'profiles.deleteThisProfileTitle' => 'Ta bort denna profil?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Ta bort ${displayName}. Anslutningar påverkas inte.', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Ta bort ${displayName}. Anslutningar påverkas inte.',
'profiles.active' => 'Aktiv', 'profiles.active' => 'Aktiv',
'profiles.manage' => 'Hantera', 'profiles.manage' => 'Hantera',
'profiles.delete' => 'Ta bort', 'profiles.delete' => 'Ta bort',
_ => null,
} ?? switch (path) {
'profiles.signOut' => 'Logga ut', 'profiles.signOut' => 'Logga ut',
'profiles.signOutPlexTitle' => 'Logga ut från Plex?', 'profiles.signOutPlexTitle' => 'Logga ut från Plex?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Ta bort ${displayName} och alla Plex Home-användare? Du kan logga in igen när som helst.', 'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Ta bort ${displayName} och alla Plex Home-användare? Du kan logga in igen när som helst.',
@@ -2801,6 +2810,7 @@ extension on TranslationsSv {
'companionRemote.remote.audio' => 'Ljud', 'companionRemote.remote.audio' => 'Ljud',
'companionRemote.remote.searchHint' => 'Sök på datorn...', 'companionRemote.remote.searchHint' => 'Sök på datorn...',
'videoSettings.playbackSpeed' => 'Uppspelningshastighet', 'videoSettings.playbackSpeed' => 'Uppspelningshastighet',
'videoSettings.zoom' => 'Zoom',
'videoSettings.sleepTimer' => 'Sovtimer', 'videoSettings.sleepTimer' => 'Sovtimer',
'videoSettings.audioSync' => 'Ljudsynkronisering', 'videoSettings.audioSync' => 'Ljudsynkronisering',
'videoSettings.subtitleSync' => 'Undertextsynkronisering', 'videoSettings.subtitleSync' => 'Undertextsynkronisering',
@@ -2829,13 +2839,13 @@ extension on TranslationsSv {
'metadataEdit.basicInfo' => 'Grundläggande info', 'metadataEdit.basicInfo' => 'Grundläggande info',
'metadataEdit.artwork' => 'Artwork', 'metadataEdit.artwork' => 'Artwork',
'metadataEdit.advancedSettings' => 'Avancerade inställningar', 'metadataEdit.advancedSettings' => 'Avancerade inställningar',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => 'Titel', 'metadataEdit.title' => 'Titel',
'metadataEdit.sortTitle' => 'Sorteringstitel', 'metadataEdit.sortTitle' => 'Sorteringstitel',
'metadataEdit.originalTitle' => 'Originaltitel', 'metadataEdit.originalTitle' => 'Originaltitel',
'metadataEdit.releaseDate' => 'Utgivningsdatum', 'metadataEdit.releaseDate' => 'Utgivningsdatum',
'metadataEdit.contentRating' => 'Åldersgräns', 'metadataEdit.contentRating' => 'Åldersgräns',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => 'Studio', 'metadataEdit.studio' => 'Studio',
'metadataEdit.tagline' => 'Tagline', 'metadataEdit.tagline' => 'Tagline',
'metadataEdit.summary' => 'Sammanfattning', 'metadataEdit.summary' => 'Sammanfattning',
+14 -4
View File
@@ -605,6 +605,7 @@ class _TranslationsVideoControlsZh extends TranslationsVideoControlsEn {
@override String get pipActive => '正在画中画模式中播放'; @override String get pipActive => '正在画中画模式中播放';
@override String get pipFailed => '画中画启动失败'; @override String get pipFailed => '画中画启动失败';
@override String get screenshotSaved => '截图已保存'; @override String get screenshotSaved => '截图已保存';
@override String zoomPercent({required Object percent}) => '缩放 ${percent}%';
@override late final _TranslationsVideoControlsPipErrorsZh pipErrors = _TranslationsVideoControlsPipErrorsZh._(_root); @override late final _TranslationsVideoControlsPipErrorsZh pipErrors = _TranslationsVideoControlsPipErrorsZh._(_root);
@override String get chapters => '章节'; @override String get chapters => '章节';
@override String get noChaptersAvailable => '没有可用的章节'; @override String get noChaptersAvailable => '没有可用的章节';
@@ -1308,6 +1309,7 @@ class _TranslationsVideoSettingsZh extends TranslationsVideoSettingsEn {
// Translations // Translations
@override String get playbackSpeed => '播放速度'; @override String get playbackSpeed => '播放速度';
@override String get zoom => '缩放';
@override String get sleepTimer => '睡眠定时器'; @override String get sleepTimer => '睡眠定时器';
@override String get audioSync => '音频同步'; @override String get audioSync => '音频同步';
@override String get subtitleSync => '字幕同步'; @override String get subtitleSync => '字幕同步';
@@ -1565,6 +1567,9 @@ class _TranslationsHotkeysActionsZh extends TranslationsHotkeysActionsEn {
@override String get speedIncrease => '加速'; @override String get speedIncrease => '加速';
@override String get speedDecrease => '减速'; @override String get speedDecrease => '减速';
@override String get speedReset => '重置速度'; @override String get speedReset => '重置速度';
@override String get zoomIn => '放大';
@override String get zoomOut => '缩小';
@override String get zoomReset => '重置缩放';
@override String get subSeekNext => '跳转到下一字幕'; @override String get subSeekNext => '跳转到下一字幕';
@override String get subSeekPrev => '跳转到上一字幕'; @override String get subSeekPrev => '跳转到上一字幕';
@override String get shaderToggle => '切换着色器'; @override String get shaderToggle => '切换着色器';
@@ -2099,6 +2104,9 @@ extension on TranslationsZh {
'hotkeys.actions.speedIncrease' => '加速', 'hotkeys.actions.speedIncrease' => '加速',
'hotkeys.actions.speedDecrease' => '减速', 'hotkeys.actions.speedDecrease' => '减速',
'hotkeys.actions.speedReset' => '重置速度', 'hotkeys.actions.speedReset' => '重置速度',
'hotkeys.actions.zoomIn' => '放大',
'hotkeys.actions.zoomOut' => '缩小',
'hotkeys.actions.zoomReset' => '重置缩放',
'hotkeys.actions.subSeekNext' => '跳转到下一字幕', 'hotkeys.actions.subSeekNext' => '跳转到下一字幕',
'hotkeys.actions.subSeekPrev' => '跳转到上一字幕', 'hotkeys.actions.subSeekPrev' => '跳转到上一字幕',
'hotkeys.actions.shaderToggle' => '切换着色器', 'hotkeys.actions.shaderToggle' => '切换着色器',
@@ -2224,6 +2232,7 @@ extension on TranslationsZh {
'videoControls.pipActive' => '正在画中画模式中播放', 'videoControls.pipActive' => '正在画中画模式中播放',
'videoControls.pipFailed' => '画中画启动失败', 'videoControls.pipFailed' => '画中画启动失败',
'videoControls.screenshotSaved' => '截图已保存', 'videoControls.screenshotSaved' => '截图已保存',
'videoControls.zoomPercent' => ({required Object percent}) => '缩放 ${percent}%',
'videoControls.pipErrors.androidVersion' => '需要 Android 8.0 或更高版本', 'videoControls.pipErrors.androidVersion' => '需要 Android 8.0 或更高版本',
'videoControls.pipErrors.iosVersion' => '需要 iOS 15.0 或更高版本', 'videoControls.pipErrors.iosVersion' => '需要 iOS 15.0 或更高版本',
'videoControls.pipErrors.permissionDisabled' => '画中画已禁用。请在系统设置中启用。', 'videoControls.pipErrors.permissionDisabled' => '画中画已禁用。请在系统设置中启用。',
@@ -2316,12 +2325,12 @@ extension on TranslationsZh {
'profiles.addPlezyProfile' => '添加 Plezy 配置文件', 'profiles.addPlezyProfile' => '添加 Plezy 配置文件',
'profiles.switchingProfile' => '切换配置文件中…', 'profiles.switchingProfile' => '切换配置文件中…',
'profiles.deleteThisProfileTitle' => '删除此配置文件?', 'profiles.deleteThisProfileTitle' => '删除此配置文件?',
_ => null,
} ?? switch (path) {
'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '移除 ${displayName}。连接不会受影响。', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '移除 ${displayName}。连接不会受影响。',
'profiles.active' => '活跃', 'profiles.active' => '活跃',
'profiles.manage' => '管理', 'profiles.manage' => '管理',
'profiles.delete' => '删除', 'profiles.delete' => '删除',
_ => null,
} ?? switch (path) {
'profiles.signOut' => '退出登录', 'profiles.signOut' => '退出登录',
'profiles.signOutPlexTitle' => '退出 Plex 登录?', 'profiles.signOutPlexTitle' => '退出 Plex 登录?',
'profiles.signOutPlexMessage' => ({required Object displayName}) => '要移除 ${displayName} 和所有 Plex Home 用户吗?可随时重新登录。', 'profiles.signOutPlexMessage' => ({required Object displayName}) => '要移除 ${displayName} 和所有 Plex Home 用户吗?可随时重新登录。',
@@ -2801,6 +2810,7 @@ extension on TranslationsZh {
'companionRemote.remote.audio' => '音频', 'companionRemote.remote.audio' => '音频',
'companionRemote.remote.searchHint' => '在桌面上搜索...', 'companionRemote.remote.searchHint' => '在桌面上搜索...',
'videoSettings.playbackSpeed' => '播放速度', 'videoSettings.playbackSpeed' => '播放速度',
'videoSettings.zoom' => '缩放',
'videoSettings.sleepTimer' => '睡眠定时器', 'videoSettings.sleepTimer' => '睡眠定时器',
'videoSettings.audioSync' => '音频同步', 'videoSettings.audioSync' => '音频同步',
'videoSettings.subtitleSync' => '字幕同步', 'videoSettings.subtitleSync' => '字幕同步',
@@ -2829,13 +2839,13 @@ extension on TranslationsZh {
'metadataEdit.basicInfo' => '基本信息', 'metadataEdit.basicInfo' => '基本信息',
'metadataEdit.artwork' => '封面图片', 'metadataEdit.artwork' => '封面图片',
'metadataEdit.advancedSettings' => '高级设置', 'metadataEdit.advancedSettings' => '高级设置',
_ => null,
} ?? switch (path) {
'metadataEdit.title' => '标题', 'metadataEdit.title' => '标题',
'metadataEdit.sortTitle' => '排序标题', 'metadataEdit.sortTitle' => '排序标题',
'metadataEdit.originalTitle' => '原始标题', 'metadataEdit.originalTitle' => '原始标题',
'metadataEdit.releaseDate' => '上映日期', 'metadataEdit.releaseDate' => '上映日期',
'metadataEdit.contentRating' => '内容分级', 'metadataEdit.contentRating' => '内容分级',
_ => null,
} ?? switch (path) {
'metadataEdit.studio' => '制片厂', 'metadataEdit.studio' => '制片厂',
'metadataEdit.tagline' => '标语', 'metadataEdit.tagline' => '标语',
'metadataEdit.summary' => '简介', 'metadataEdit.summary' => '简介',
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "Öka hastighet", "speedIncrease": "Öka hastighet",
"speedDecrease": "Minska hastighet", "speedDecrease": "Minska hastighet",
"speedReset": "Återställ hastighet", "speedReset": "Återställ hastighet",
"zoomIn": "Zooma in",
"zoomOut": "Zooma ut",
"zoomReset": "Återställ zoom",
"subSeekNext": "Hoppa till nästa undertext", "subSeekNext": "Hoppa till nästa undertext",
"subSeekPrev": "Hoppa till föregående undertext", "subSeekPrev": "Hoppa till föregående undertext",
"shaderToggle": "Växla shaders", "shaderToggle": "Växla shaders",
@@ -444,6 +447,7 @@
"pipActive": "Spelar i bild-i-bild", "pipActive": "Spelar i bild-i-bild",
"pipFailed": "Bild-i-bild kunde inte starta", "pipFailed": "Bild-i-bild kunde inte starta",
"screenshotSaved": "Skärmbild sparad", "screenshotSaved": "Skärmbild sparad",
"zoomPercent": "Zoom ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "Kräver Android 8.0 eller nyare", "androidVersion": "Kräver Android 8.0 eller nyare",
"iosVersion": "Kräver iOS 15.0 eller nyare", "iosVersion": "Kräver iOS 15.0 eller nyare",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "Uppspelningshastighet", "playbackSpeed": "Uppspelningshastighet",
"zoom": "Zoom",
"sleepTimer": "Sovtimer", "sleepTimer": "Sovtimer",
"audioSync": "Ljudsynkronisering", "audioSync": "Ljudsynkronisering",
"subtitleSync": "Undertextsynkronisering", "subtitleSync": "Undertextsynkronisering",
+5
View File
@@ -306,6 +306,9 @@
"speedIncrease": "加速", "speedIncrease": "加速",
"speedDecrease": "减速", "speedDecrease": "减速",
"speedReset": "重置速度", "speedReset": "重置速度",
"zoomIn": "放大",
"zoomOut": "缩小",
"zoomReset": "重置缩放",
"subSeekNext": "跳转到下一字幕", "subSeekNext": "跳转到下一字幕",
"subSeekPrev": "跳转到上一字幕", "subSeekPrev": "跳转到上一字幕",
"shaderToggle": "切换着色器", "shaderToggle": "切换着色器",
@@ -444,6 +447,7 @@
"pipActive": "正在画中画模式中播放", "pipActive": "正在画中画模式中播放",
"pipFailed": "画中画启动失败", "pipFailed": "画中画启动失败",
"screenshotSaved": "截图已保存", "screenshotSaved": "截图已保存",
"zoomPercent": "缩放 ${percent}%",
"pipErrors": { "pipErrors": {
"androidVersion": "需要 Android 8.0 或更高版本", "androidVersion": "需要 Android 8.0 或更高版本",
"iosVersion": "需要 iOS 15.0 或更高版本", "iosVersion": "需要 iOS 15.0 或更高版本",
@@ -1083,6 +1087,7 @@
}, },
"videoSettings": { "videoSettings": {
"playbackSpeed": "播放速度", "playbackSpeed": "播放速度",
"zoom": "缩放",
"sleepTimer": "睡眠定时器", "sleepTimer": "睡眠定时器",
"audioSync": "音频同步", "audioSync": "音频同步",
"subtitleSync": "字幕同步", "subtitleSync": "字幕同步",
@@ -372,6 +372,12 @@ class PlayerAndroid extends PlayerBase {
await invoke('setBoxFitMode', {'mode': mode}); await invoke('setBoxFitMode', {'mode': mode});
} }
/// Apply custom zoom to the native ExoPlayer layer.
Future<void> setVideoZoom(double scale) async {
if (disposed || !initialized) return;
await invoke('setVideoZoom', {'scale': scale});
}
@override @override
Future<bool> setVideoFrameRate(double fps, int durationMs, {int extraDelayMs = 0}) async { Future<bool> setVideoFrameRate(double fps, int durationMs, {int extraDelayMs = 0}) async {
if (disposed || !initialized) return false; if (disposed || !initialized) return false;
+42 -9
View File
@@ -2,6 +2,7 @@ part of '../../video_player_screen.dart';
extension _VideoPlayerBuildMethods on VideoPlayerScreenState { extension _VideoPlayerBuildMethods on VideoPlayerScreenState {
static const double _videoLayoutSizeTolerance = 0.1; static const double _videoLayoutSizeTolerance = 0.1;
static const double _pinchZoomActivationThreshold = 0.015;
bool _isSameVideoLayoutSize(Size a, Size b) { bool _isSameVideoLayoutSize(Size a, Size b) {
return (a.width - b.width).abs() <= _videoLayoutSizeTolerance && return (a.width - b.width).abs() <= _videoLayoutSizeTolerance &&
@@ -138,6 +139,22 @@ extension _VideoPlayerBuildMethods on VideoPlayerScreenState {
); );
} }
void _startMobileZoomGesture() {
final filterManager = _videoFilterManager;
if (filterManager == null || _isPinchZooming) return;
_ambientLightingService?.disable();
_isPinchZooming = true;
_pinchZoomChanged = false;
_pinchStartZoomScale = filterManager.zoomScale;
}
void _clearMobileZoomGesture() {
_isPinchZooming = false;
_pinchZoomChanged = false;
_pinchStartZoomScale = null;
}
Widget _buildVideoPlayer(BuildContext context) { Widget _buildVideoPlayer(BuildContext context) {
// Cache platform detection to avoid multiple calls // Cache platform detection to avoid multiple calls
final isMobile = PlatformDetector.isMobile(context); final isMobile = PlatformDetector.isMobile(context);
@@ -167,22 +184,33 @@ extension _VideoPlayerBuildMethods on VideoPlayerScreenState {
behavior: HitTestBehavior.translucent, // Allow taps to pass through to controls behavior: HitTestBehavior.translucent, // Allow taps to pass through to controls
onScaleStart: (details) { onScaleStart: (details) {
if (!isMobile) return; if (!isMobile) return;
if (_videoFilterManager != null) { if (details.pointerCount >= 2) _startMobileZoomGesture();
_videoFilterManager!.isPinching = false;
}
}, },
onScaleUpdate: (details) { onScaleUpdate: (details) {
if (!isMobile) return; if (!isMobile) return;
if (details.pointerCount >= 2 && _videoFilterManager != null) { if (details.pointerCount < 2) return;
_videoFilterManager!.isPinching = true; if (!_isPinchZooming) _startMobileZoomGesture();
}
final startZoom = _pinchStartZoomScale;
final filterManager = _videoFilterManager;
if (!_isPinchZooming || startZoom == null || filterManager == null) return;
if ((details.scale - 1.0).abs() <= _pinchZoomActivationThreshold && !_pinchZoomChanged) return;
_pinchZoomChanged = true;
filterManager.setZoomScale(startZoom * details.scale);
}, },
onScaleEnd: (details) { onScaleEnd: (details) {
if (!isMobile) return; if (!isMobile) return;
if (_videoFilterManager != null && _videoFilterManager!.isPinching) { if (!_isPinchZooming) return;
_toggleContainCover(); if (!_pinchZoomChanged) {
_videoFilterManager!.isPinching = false; _clearMobileZoomGesture();
return;
} }
final zoomScale = _videoFilterManager?.zoomScale ?? 1.0;
_showZoomToast(zoomScale);
_clearMobileZoomGesture();
_setPlayerState(() {});
}, },
child: Stack( child: Stack(
children: [ children: [
@@ -244,7 +272,12 @@ extension _VideoPlayerBuildMethods on VideoPlayerScreenState {
sourcePartId: _currentMediaInfo?.partId, sourcePartId: _currentMediaInfo?.partId,
onTogglePIPMode: _togglePIPMode, onTogglePIPMode: _togglePIPMode,
boxFitMode: _videoFilterManager?.boxFitMode ?? 0, boxFitMode: _videoFilterManager?.boxFitMode ?? 0,
videoZoomScale: _videoFilterManager?.zoomScale ?? 1.0,
onCycleBoxFitMode: _cycleBoxFitMode, onCycleBoxFitMode: _cycleBoxFitMode,
onVideoZoomChanged: _setVideoZoom,
onZoomIn: _zoomVideoIn,
onZoomOut: _zoomVideoOut,
onResetVideoZoom: _resetVideoZoom,
onCycleAudioTrack: _cycleAudioTrack, onCycleAudioTrack: _cycleAudioTrack,
onCycleSubtitleTrack: _cycleSubtitleTrack, onCycleSubtitleTrack: _cycleSubtitleTrack,
onAudioTrackChanged: _onAudioTrackChanged, onAudioTrackChanged: _onAudioTrackChanged,
+29 -7
View File
@@ -68,6 +68,35 @@ extension _VideoPlayerShaderMethods on VideoPlayerScreenState {
}); });
} }
void _showZoomToast(double zoomScale) {
_toastController.show(Symbols.zoom_in_rounded, t.videoControls.zoomPercent(percent: (zoomScale * 100).round()));
}
double _setVideoZoom(double zoomScale, {bool showToast = true}) {
final filterManager = _videoFilterManager;
if (filterManager == null) return 1.0;
_ambientLightingService?.disable();
final next = filterManager.setZoomScale(zoomScale);
if (showToast) _showZoomToast(next);
if (mounted) _setPlayerState(() {});
return next;
}
void _zoomVideoIn() {
final current = _videoFilterManager?.zoomScale ?? 1.0;
_setVideoZoom(current + VideoFilterManager.zoomStep);
}
void _zoomVideoOut() {
final current = _videoFilterManager?.zoomScale ?? 1.0;
_setVideoZoom(current - VideoFilterManager.zoomStep);
}
void _resetVideoZoom() {
_setVideoZoom(1.0);
}
/// Update video-aspect-override when player size changes. /// Update video-aspect-override when player size changes.
/// The shader adapts automatically via built-in target_size uniform. /// The shader adapts automatically via built-in target_size uniform.
void _updateAmbientLightingOnResize(Size newSize) { void _updateAmbientLightingOnResize(Size newSize) {
@@ -120,11 +149,4 @@ extension _VideoPlayerShaderMethods on VideoPlayerScreenState {
if (mounted) _setPlayerState(() {}); if (mounted) _setPlayerState(() {});
} }
/// Toggle between contain and cover modes only (for pinch gesture)
void _toggleContainCover() {
_setPlayerState(() {
_videoFilterManager?.toggleContainCover();
});
}
} }
+3
View File
@@ -400,6 +400,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Size? _pendingVideoLayoutSize; Size? _pendingVideoLayoutSize;
Player? _lastVideoLayoutPlayer; Player? _lastVideoLayoutPlayer;
bool _videoLayoutUpdateScheduled = false; bool _videoLayoutUpdateScheduled = false;
double? _pinchStartZoomScale;
bool _isPinchZooming = false;
bool _pinchZoomChanged = false;
final EpisodeNavigationService _episodeNavigation = EpisodeNavigationService(); final EpisodeNavigationService _episodeNavigation = EpisodeNavigationService();
WatchTogetherProvider? _watchTogetherProvider; WatchTogetherProvider? _watchTogetherProvider;
+33 -2
View File
@@ -12,6 +12,8 @@ import '../utils/platform_detector.dart';
import '../utils/player_utils.dart'; import '../utils/player_utils.dart';
class KeyboardShortcutsService extends ChangeNotifier { class KeyboardShortcutsService extends ChangeNotifier {
static const Set<String> _repeatableVideoActions = {'zoom_in', 'zoom_out'};
static KeyboardShortcutsService? _instance; static KeyboardShortcutsService? _instance;
late SettingsService _settingsService; late SettingsService _settingsService;
final List<VoidCallback> _settingsDisposers = []; final List<VoidCallback> _settingsDisposers = [];
@@ -183,13 +185,17 @@ class KeyboardShortcutsService extends ChangeNotifier {
VoidCallback? onNextEpisode, VoidCallback? onNextEpisode,
VoidCallback? onPreviousEpisode, VoidCallback? onPreviousEpisode,
VoidCallback? onScreenshot, VoidCallback? onScreenshot,
VoidCallback? onZoomIn,
VoidCallback? onZoomOut,
VoidCallback? onZoomReset,
int? currentPositionEpoch, int? currentPositionEpoch,
ValueChanged<int>? onLiveSeek, ValueChanged<int>? onLiveSeek,
Future<void> Function(Duration position)? onSeekRequested, Future<void> Function(Duration position)? onSeekRequested,
}) { }) {
if (event is! KeyDownEvent) return KeyEventResult.ignored; final isRepeat = event is KeyRepeatEvent;
if (event is! KeyDownEvent && !isRepeat) return KeyEventResult.ignored;
if (event.logicalKey == LogicalKeyboardKey.escape) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) {
onBack?.call(); onBack?.call();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@@ -247,6 +253,10 @@ class KeyboardShortcutsService extends ChangeNotifier {
continue; continue;
} }
if (isRepeat && !_repeatableVideoActions.contains(action)) {
return KeyEventResult.handled;
}
_executeAction( _executeAction(
action, action,
player, player,
@@ -261,6 +271,9 @@ class KeyboardShortcutsService extends ChangeNotifier {
onNextEpisode: onNextEpisode, onNextEpisode: onNextEpisode,
onPreviousEpisode: onPreviousEpisode, onPreviousEpisode: onPreviousEpisode,
onScreenshot: onScreenshot, onScreenshot: onScreenshot,
onZoomIn: onZoomIn,
onZoomOut: onZoomOut,
onZoomReset: onZoomReset,
currentPositionEpoch: currentPositionEpoch, currentPositionEpoch: currentPositionEpoch,
onLiveSeek: onLiveSeek, onLiveSeek: onLiveSeek,
onSeekRequested: onSeekRequested, onSeekRequested: onSeekRequested,
@@ -286,6 +299,9 @@ class KeyboardShortcutsService extends ChangeNotifier {
VoidCallback? onNextEpisode, VoidCallback? onNextEpisode,
VoidCallback? onPreviousEpisode, VoidCallback? onPreviousEpisode,
VoidCallback? onScreenshot, VoidCallback? onScreenshot,
VoidCallback? onZoomIn,
VoidCallback? onZoomOut,
VoidCallback? onZoomReset,
int? currentPositionEpoch, int? currentPositionEpoch,
ValueChanged<int>? onLiveSeek, ValueChanged<int>? onLiveSeek,
Future<void> Function(Duration position)? onSeekRequested, Future<void> Function(Duration position)? onSeekRequested,
@@ -383,6 +399,15 @@ class KeyboardShortcutsService extends ChangeNotifier {
case 'screenshot': case 'screenshot':
unawaited(player.command(['screenshot', 'subtitles']).then((_) => onScreenshot?.call())); unawaited(player.command(['screenshot', 'subtitles']).then((_) => onScreenshot?.call()));
break; break;
case 'zoom_in':
onZoomIn?.call();
break;
case 'zoom_out':
onZoomOut?.call();
break;
case 'zoom_reset':
onZoomReset?.call();
break;
} }
} }
@@ -436,6 +461,12 @@ class KeyboardShortcutsService extends ChangeNotifier {
return t.hotkeys.actions.skipMarker; return t.hotkeys.actions.skipMarker;
case 'screenshot': case 'screenshot':
return t.hotkeys.actions.screenshot; return t.hotkeys.actions.screenshot;
case 'zoom_in':
return t.hotkeys.actions.zoomIn;
case 'zoom_out':
return t.hotkeys.actions.zoomOut;
case 'zoom_reset':
return t.hotkeys.actions.zoomReset;
default: default:
return action; return action;
} }
+6
View File
@@ -216,6 +216,9 @@ Map<String, String> _defaultKeyboardShortcuts() => {
'speed_increase': 'Plus', 'speed_increase': 'Plus',
'speed_decrease': 'Minus', 'speed_decrease': 'Minus',
'speed_reset': 'R', 'speed_reset': 'R',
'zoom_in': 'Alt+Plus',
'zoom_out': 'Alt+Minus',
'zoom_reset': 'Alt+Backspace',
'sub_seek_next': 'Ctrl+Right', 'sub_seek_next': 'Ctrl+Right',
'sub_seek_prev': 'Ctrl+Left', 'sub_seek_prev': 'Ctrl+Left',
'screenshot': 'Ctrl+S', 'screenshot': 'Ctrl+S',
@@ -241,6 +244,9 @@ Map<String, HotKey> _defaultKeyboardHotkeys() => {
'speed_increase': const HotKey(key: PhysicalKeyboardKey.equal), 'speed_increase': const HotKey(key: PhysicalKeyboardKey.equal),
'speed_decrease': const HotKey(key: PhysicalKeyboardKey.minus), 'speed_decrease': const HotKey(key: PhysicalKeyboardKey.minus),
'speed_reset': const HotKey(key: PhysicalKeyboardKey.keyR), 'speed_reset': const HotKey(key: PhysicalKeyboardKey.keyR),
'zoom_in': const HotKey(key: PhysicalKeyboardKey.equal, modifiers: [HotKeyModifier.alt]),
'zoom_out': const HotKey(key: PhysicalKeyboardKey.minus, modifiers: [HotKeyModifier.alt]),
'zoom_reset': const HotKey(key: PhysicalKeyboardKey.backspace, modifiers: [HotKeyModifier.alt]),
'sub_seek_next': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]), 'sub_seek_next': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]),
'sub_seek_prev': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]), 'sub_seek_prev': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]),
'shader_toggle': const HotKey(key: PhysicalKeyboardKey.keyG), 'shader_toggle': const HotKey(key: PhysicalKeyboardKey.keyG),
+53 -12
View File
@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rate_limiter/rate_limiter.dart'; import 'package:rate_limiter/rate_limiter.dart';
@@ -17,6 +19,10 @@ import 'ambient_lighting_service.dart';
/// - Debounced video filter updates on resize events /// - Debounced video filter updates on resize events
/// - Ambient-lighting-friendly reset to contain mode /// - Ambient-lighting-friendly reset to contain mode
class VideoFilterManager { class VideoFilterManager {
static const double minZoomScale = 0.5;
static const double maxZoomScale = 2.0;
static const double zoomStep = 0.01;
final Player player; final Player player;
final List<MediaVersion> availableVersions; final List<MediaVersion> availableVersions;
final int selectedMediaIndex; final int selectedMediaIndex;
@@ -27,14 +33,17 @@ class VideoFilterManager {
/// Store the boxFitMode before entering PiP so it can be restored /// Store the boxFitMode before entering PiP so it can be restored
int? _prePipBoxFitMode; int? _prePipBoxFitMode;
/// Store the zoom level before entering PiP so it can be restored
double? _prePipZoomScale;
/// Store whether ambient lighting was active before entering PiP /// Store whether ambient lighting was active before entering PiP
bool? _prePipAmbientLighting; bool? _prePipAmbientLighting;
/// Ambient lighting service reference - when active, video-aspect-override is managed by ambient lighting /// Ambient lighting service reference - when active, video-aspect-override is managed by ambient lighting
AmbientLightingService? ambientLightingService; AmbientLightingService? ambientLightingService;
/// Track if a pinch gesture is occurring (public for gesture tracking) /// Custom video zoom layered on top of the selected fit mode.
bool isPinching = false; double _zoomScale = 1.0;
/// Current player viewport size /// Current player viewport size
Size? _playerSize; Size? _playerSize;
@@ -63,8 +72,27 @@ class VideoFilterManager {
/// Current BoxFit mode (0=contain, 1=cover, 2=fill) /// Current BoxFit mode (0=contain, 1=cover, 2=fill)
int get boxFitMode => _boxFitMode; int get boxFitMode => _boxFitMode;
double get zoomScale => _zoomScale;
Size? get playerSize => _playerSize; Size? get playerSize => _playerSize;
static double videoZoomPropertyForScale(double scale) {
final clamped = scale.clamp(minZoomScale, maxZoomScale).toDouble();
return math.log(clamped) / math.ln2;
}
double setZoomScale(double scale) {
final next = scale.clamp(minZoomScale, maxZoomScale).toDouble();
if ((_zoomScale - next).abs() < 0.0001) return _zoomScale;
_zoomScale = next;
updateVideoFilter();
return _zoomScale;
}
double adjustZoom(double delta) => setZoomScale(_zoomScale + delta);
double resetZoom() => setZoomScale(1.0);
/// Cycle through BoxFit modes: contain → cover → fill → contain (for button) /// Cycle through BoxFit modes: contain → cover → fill → contain (for button)
void cycleBoxFitMode() { void cycleBoxFitMode() {
_boxFitMode = (_boxFitMode + 1) % 3; _boxFitMode = (_boxFitMode + 1) % 3;
@@ -74,19 +102,13 @@ class VideoFilterManager {
/// Reset to contain mode (mode 0). Used when enabling ambient lighting. /// Reset to contain mode (mode 0). Used when enabling ambient lighting.
void resetToContain() { void resetToContain() {
if (_boxFitMode != 0) { if (_boxFitMode != 0 || (_zoomScale - 1.0).abs() > 0.0001) {
_boxFitMode = 0; _boxFitMode = 0;
_zoomScale = 1.0;
updateVideoFilter(); updateVideoFilter();
} }
} }
/// Toggle between contain and cover modes only (for pinch gesture)
void toggleContainCover() {
_boxFitMode = _boxFitMode == 0 ? 1 : 0;
onBoxFitModeChanged?.call(_boxFitMode);
updateVideoFilter();
}
/// Force contain mode for PiP (no cropping/stretching) /// Force contain mode for PiP (no cropping/stretching)
void enterPipMode() { void enterPipMode() {
// Disable ambient lighting for PiP — it wastes space on blurred borders // Disable ambient lighting for PiP — it wastes space on blurred borders
@@ -97,17 +119,30 @@ class VideoFilterManager {
if (_boxFitMode != 0) { if (_boxFitMode != 0) {
_prePipBoxFitMode = _boxFitMode; _prePipBoxFitMode = _boxFitMode;
_boxFitMode = 0; // Contain mode _boxFitMode = 0; // Contain mode
}
if ((_zoomScale - 1.0).abs() > 0.0001) {
_prePipZoomScale = _zoomScale;
_zoomScale = 1.0;
}
if (_prePipBoxFitMode != null || _prePipZoomScale != null) {
updateVideoFilter(); updateVideoFilter();
} }
} }
/// Restore previous mode when exiting PiP /// Restore previous mode when exiting PiP
void exitPipMode() { void exitPipMode() {
var shouldUpdate = false;
if (_prePipBoxFitMode != null) { if (_prePipBoxFitMode != null) {
_boxFitMode = _prePipBoxFitMode!; _boxFitMode = _prePipBoxFitMode!;
_prePipBoxFitMode = null; _prePipBoxFitMode = null;
updateVideoFilter(); shouldUpdate = true;
} }
if (_prePipZoomScale != null) {
_zoomScale = _prePipZoomScale!;
_prePipZoomScale = null;
shouldUpdate = true;
}
if (shouldUpdate) updateVideoFilter();
} }
/// Whether ambient lighting was active before entering PiP /// Whether ambient lighting was active before entering PiP
@@ -129,13 +164,14 @@ class VideoFilterManager {
/// Update the video scaling and positioning based on current display mode. /// Update the video scaling and positioning based on current display mode.
/// When ambient lighting is active, video-aspect-override is managed by ambient lighting. /// When ambient lighting is active, video-aspect-override is managed by ambient lighting.
void updateVideoFilter() async { Future<void> updateVideoFilter() async {
try { try {
// ExoPlayer handles scaling via AspectRatioFrameLayout. The MPV properties // ExoPlayer handles scaling via AspectRatioFrameLayout. The MPV properties
// below still run — on PlayerAndroid they forward to setMpvProperty, which // below still run — on PlayerAndroid they forward to setMpvProperty, which
// queues them for any future fallback to MPV. // queues them for any future fallback to MPV.
if (player is PlayerAndroid) { if (player is PlayerAndroid) {
await (player as PlayerAndroid).setBoxFitMode(_boxFitMode); await (player as PlayerAndroid).setBoxFitMode(_boxFitMode);
await (player as PlayerAndroid).setVideoZoom(_zoomScale);
} }
if (ambientLightingService?.isEnabled != true) { if (ambientLightingService?.isEnabled != true) {
@@ -143,6 +179,7 @@ class VideoFilterManager {
} }
await player.setProperty('sub-ass-force-margins', 'no'); await player.setProperty('sub-ass-force-margins', 'no');
await player.setProperty('panscan', '0'); await player.setProperty('panscan', '0');
await player.setProperty('video-zoom', videoZoomPropertyForScale(_zoomScale).toString());
if (_boxFitMode == 1) { if (_boxFitMode == 1) {
// Cover mode - use panscan to fill screen while maintaining aspect ratio // Cover mode - use panscan to fill screen while maintaining aspect ratio
@@ -156,6 +193,10 @@ class VideoFilterManager {
appLogger.d('Stretch mode: aspect-override=$playerAspect (player: $_playerSize)'); appLogger.d('Stretch mode: aspect-override=$playerAspect (player: $_playerSize)');
} }
} }
if (_zoomScale > 1.0001) {
await player.setProperty('sub-ass-force-margins', 'yes');
}
} catch (e) { } catch (e) {
appLogger.w('Failed to update video filter', error: e); appLogger.w('Failed to update video filter', error: e);
} }
@@ -0,0 +1,136 @@
import 'dart:ui' show Offset;
import 'package:flutter/gestures.dart' show kDoubleTapSlop, kDoubleTapTimeout, kDoubleTapTouchSlop;
class TwoFingerDoubleTapTracker {
TwoFingerDoubleTapTracker({
DateTime Function()? now,
this.tapTimeout = kDoubleTapTimeout,
this.doubleTapTimeout = kDoubleTapTimeout,
this.tapSlop = kDoubleTapTouchSlop,
this.doubleTapSlop = kDoubleTapSlop,
}) : _now = now ?? DateTime.now;
final DateTime Function() _now;
final Duration tapTimeout;
final Duration doubleTapTimeout;
final double tapSlop;
final double doubleTapSlop;
final Map<int, _TrackedTouch> _activeTouches = {};
DateTime? _candidateStartTime;
Offset? _candidateFocalPoint;
bool _candidateActive = false;
bool _candidateInvalid = false;
DateTime? _lastTapTime;
Offset? _lastTapFocalPoint;
bool get isChordActive => _activeTouches.length > 1 || _candidateActive;
void pointerDown(int pointer, Offset position) {
_activeTouches[pointer] = _TrackedTouch(start: position, current: position, downTime: _now());
if (_activeTouches.length == 2) {
_candidateActive = true;
_candidateInvalid = false;
_candidateStartTime = _earliestActiveDownTime();
_candidateFocalPoint = _activeFocalPoint();
} else if (_activeTouches.length > 2) {
_candidateInvalid = true;
}
}
void pointerMove(int pointer, Offset position) {
final touch = _activeTouches[pointer];
if (touch == null) return;
touch.current = position;
if ((position - touch.start).distance > tapSlop) {
_candidateInvalid = true;
}
}
bool pointerUp(int pointer, Offset position) {
final touch = _activeTouches[pointer];
if (touch == null) return false;
touch.current = position;
if ((position - touch.start).distance > tapSlop) {
_candidateInvalid = true;
}
_activeTouches.remove(pointer);
if (_activeTouches.isNotEmpty) return false;
final startTime = _candidateStartTime;
final focalPoint = _candidateFocalPoint;
final isTap =
_candidateActive &&
!_candidateInvalid &&
startTime != null &&
focalPoint != null &&
_now().difference(startTime) <= tapTimeout;
_clearCandidate();
if (!isTap) return false;
return _recordTwoFingerTap(focalPoint);
}
void pointerCancel(int pointer) {
_activeTouches.remove(pointer);
_candidateInvalid = true;
if (_activeTouches.isEmpty) _clearCandidate();
}
void resetSeries() {
_lastTapTime = null;
_lastTapFocalPoint = null;
}
DateTime _earliestActiveDownTime() {
return _activeTouches.values.map((touch) => touch.downTime).reduce((a, b) => a.isBefore(b) ? a : b);
}
Offset _activeFocalPoint() {
var sum = Offset.zero;
for (final touch in _activeTouches.values) {
sum += touch.current;
}
return sum / _activeTouches.length.toDouble();
}
bool _recordTwoFingerTap(Offset focalPoint) {
final now = _now();
final lastTapTime = _lastTapTime;
final lastTapFocalPoint = _lastTapFocalPoint;
final isDoubleTap =
lastTapTime != null &&
lastTapFocalPoint != null &&
now.difference(lastTapTime) <= doubleTapTimeout &&
(focalPoint - lastTapFocalPoint).distance <= doubleTapSlop;
if (isDoubleTap) {
resetSeries();
return true;
}
_lastTapTime = now;
_lastTapFocalPoint = focalPoint;
return false;
}
void _clearCandidate() {
_candidateStartTime = null;
_candidateFocalPoint = null;
_candidateActive = false;
_candidateInvalid = false;
}
}
class _TrackedTouch {
_TrackedTouch({required this.start, required this.current, required this.downTime});
final Offset start;
Offset current;
final DateTime downTime;
}
@@ -24,6 +24,7 @@ class TrackControlsState {
/// to show estimated file sizes per preset (bitrate × duration). /// to show estimated file sizes per preset (bitrate × duration).
final int? sourceDurationMs; final int? sourceDurationMs;
final int boxFitMode; final int boxFitMode;
final double videoZoomScale;
final int audioSyncOffset; final int audioSyncOffset;
final int subtitleSyncOffset; final int subtitleSyncOffset;
final bool isRotationLocked; final bool isRotationLocked;
@@ -32,6 +33,8 @@ class TrackControlsState {
final bool isAlwaysOnTop; final bool isAlwaysOnTop;
final VoidCallback? onTogglePIPMode; final VoidCallback? onTogglePIPMode;
final VoidCallback? onCycleBoxFitMode; final VoidCallback? onCycleBoxFitMode;
final ValueChanged<double>? onVideoZoomChanged;
final VoidCallback? onResetVideoZoom;
final VoidCallback? onToggleRotationLock; final VoidCallback? onToggleRotationLock;
final VoidCallback? onToggleScreenLock; final VoidCallback? onToggleScreenLock;
final VoidCallback? onToggleFullscreen; final VoidCallback? onToggleFullscreen;
@@ -80,6 +83,7 @@ class TrackControlsState {
this.sourcePartId, this.sourcePartId,
this.sourceDurationMs, this.sourceDurationMs,
this.boxFitMode = 0, this.boxFitMode = 0,
this.videoZoomScale = 1.0,
this.audioSyncOffset = 0, this.audioSyncOffset = 0,
this.subtitleSyncOffset = 0, this.subtitleSyncOffset = 0,
this.isRotationLocked = false, this.isRotationLocked = false,
@@ -88,6 +92,8 @@ class TrackControlsState {
this.isAlwaysOnTop = false, this.isAlwaysOnTop = false,
this.onTogglePIPMode, this.onTogglePIPMode,
this.onCycleBoxFitMode, this.onCycleBoxFitMode,
this.onVideoZoomChanged,
this.onResetVideoZoom,
this.onToggleRotationLock, this.onToggleRotationLock,
this.onToggleScreenLock, this.onToggleScreenLock,
this.onToggleFullscreen, this.onToggleFullscreen,
@@ -156,6 +156,9 @@ extension _PlexVideoControlsKeyEventMethods on _PlexVideoControlsState {
onNextEpisode: widget.onNext, onNextEpisode: widget.onNext,
onPreviousEpisode: widget.onPrevious, onPreviousEpisode: widget.onPrevious,
onScreenshot: _showScreenshotToast, onScreenshot: _showScreenshotToast,
onZoomIn: widget.onZoomIn,
onZoomOut: widget.onZoomOut,
onZoomReset: widget.onResetVideoZoom,
currentPositionEpoch: widget.currentPositionEpoch, currentPositionEpoch: widget.currentPositionEpoch,
onLiveSeek: widget.onLiveSeek, onLiveSeek: widget.onLiveSeek,
); );
@@ -304,12 +307,14 @@ extension _PlexVideoControlsKeyEventMethods on _PlexVideoControlsState {
onNextEpisode: widget.onNext, onNextEpisode: widget.onNext,
onPreviousEpisode: widget.onPrevious, onPreviousEpisode: widget.onPrevious,
onScreenshot: _showScreenshotToast, onScreenshot: _showScreenshotToast,
onZoomIn: widget.onZoomIn,
onZoomOut: widget.onZoomOut,
onZoomReset: widget.onResetVideoZoom,
currentPositionEpoch: widget.currentPositionEpoch, currentPositionEpoch: widget.currentPositionEpoch,
onLiveSeek: widget.onLiveSeek, onLiveSeek: widget.onLiveSeek,
onSeekRequested: widget.onSeekRequested, onSeekRequested: widget.onSeekRequested,
); );
// Let non-navigation keys (volume, etc.) pass through to the OS. if (!event.logicalKey.isNavigationKey) return result;
if (!event.logicalKey.isNavigationKey) return KeyEventResult.ignored;
// Never return .ignored for navigation keys — prevent leaking to previous routes. // Never return .ignored for navigation keys — prevent leaking to previous routes.
return result == KeyEventResult.ignored ? KeyEventResult.handled : result; return result == KeyEventResult.ignored ? KeyEventResult.handled : result;
} }
@@ -1,6 +1,8 @@
part of '../video_controls.dart'; part of '../video_controls.dart';
extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState { extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
static const Duration _touchTapSuppressionPadding = Duration(milliseconds: 80);
void _onRateChanged(double newRate) { void _onRateChanged(double newRate) {
if (!mounted) return; if (!mounted) return;
if (_isLongPressing) return; if (_isLongPressing) return;
@@ -95,9 +97,52 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
unawaited(_seekToPosition(position)); unawaited(_seekToPosition(position));
} }
bool get _isTouchTapSuppressed {
final until = _suppressTouchTapUntil;
if (until == null) return false;
if (DateTime.now().isAfter(until)) {
_suppressTouchTapUntil = null;
return false;
}
return true;
}
void _suppressTouchTaps() {
_singleTapTimer?.cancel();
_singleTapTimer = null;
_suppressTouchTapUntil = DateTime.now().add(kDoubleTapTimeout + _touchTapSuppressionPadding);
}
void _handleTouchPointerDown(PointerDownEvent event) {
if (event.kind != PointerDeviceKind.touch) return;
_twoFingerDoubleTapTracker.pointerDown(event.pointer, event.position);
if (_twoFingerDoubleTapTracker.isChordActive) _suppressTouchTaps();
}
void _handleTouchPointerMove(PointerMoveEvent event) {
if (event.kind != PointerDeviceKind.touch) return;
_twoFingerDoubleTapTracker.pointerMove(event.pointer, event.position);
if (_twoFingerDoubleTapTracker.isChordActive) _suppressTouchTaps();
}
void _handleTouchPointerUp(PointerUpEvent event) {
if (event.kind != PointerDeviceKind.touch) return;
final isResetGesture = _twoFingerDoubleTapTracker.pointerUp(event.pointer, event.position);
if (_isTouchTapSuppressed || isResetGesture) _suppressTouchTaps();
if (isResetGesture) widget.onResetVideoZoom?.call();
}
void _handleTouchPointerCancel(PointerCancelEvent event) {
if (event.kind != PointerDeviceKind.touch) return;
_twoFingerDoubleTapTracker.pointerCancel(event.pointer);
if (_twoFingerDoubleTapTracker.isChordActive) _suppressTouchTaps();
}
/// Timing-based double-click detection: avoids `onDoubleTap`'s ~300 ms /// Timing-based double-click detection: avoids `onDoubleTap`'s ~300 ms
/// tap-resolution delay and the arena competition it introduces. /// tap-resolution delay and the arena competition it introduces.
void _handleOuterTap() { void _handleOuterTap() {
if (PlatformDetector.isMobile(context) && _isTouchTapSuppressed) return;
if (widget.canControl && _clickVideoTogglesPlayback) { if (widget.canControl && _clickVideoTogglesPlayback) {
_playOrPause(); _playOrPause();
} else { } else {
@@ -117,6 +162,8 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
/// Handle tap in skip zone with custom double-tap detection /// Handle tap in skip zone with custom double-tap detection
void _handleTapInSkipZone({required bool isForward}) { void _handleTapInSkipZone({required bool isForward}) {
if (_isTouchTapSuppressed) return;
final now = DateTime.now(); final now = DateTime.now();
// Cancel any pending single-tap action // Cancel any pending single-tap action
@@ -253,6 +300,8 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
return; return;
} }
if (_isTouchTapSuppressed) return;
final skipZone = mobileSkipZoneForTap(position: details.localPosition, size: size); final skipZone = mobileSkipZoneForTap(position: details.localPosition, size: size);
if (skipZone != null) { if (skipZone != null) {
_handleTapInSkipZone(isForward: skipZone); _handleTapInSkipZone(isForward: skipZone);
@@ -101,6 +101,7 @@ extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState {
sourcePartId: canSwitchSourceSubtitles ? widget.sourcePartId : null, sourcePartId: canSwitchSourceSubtitles ? widget.sourcePartId : null,
sourceDurationMs: widget.metadata.durationMs, sourceDurationMs: widget.metadata.durationMs,
boxFitMode: widget.boxFitMode, boxFitMode: widget.boxFitMode,
videoZoomScale: widget.videoZoomScale,
audioSyncOffset: _audioSyncOffset, audioSyncOffset: _audioSyncOffset,
subtitleSyncOffset: _subtitleSyncOffset, subtitleSyncOffset: _subtitleSyncOffset,
isRotationLocked: _isRotationLocked, isRotationLocked: _isRotationLocked,
@@ -109,6 +110,8 @@ extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState {
isAlwaysOnTop: _isAlwaysOnTop, isAlwaysOnTop: _isAlwaysOnTop,
onTogglePIPMode: (_isPipSupported && !PlatformDetector.isTV()) ? widget.onTogglePIPMode : null, onTogglePIPMode: (_isPipSupported && !PlatformDetector.isTV()) ? widget.onTogglePIPMode : null,
onCycleBoxFitMode: widget.onCycleBoxFitMode, onCycleBoxFitMode: widget.onCycleBoxFitMode,
onVideoZoomChanged: widget.onVideoZoomChanged,
onResetVideoZoom: widget.onResetVideoZoom,
onToggleRotationLock: _toggleRotationLock, onToggleRotationLock: _toggleRotationLock,
onToggleScreenLock: _toggleScreenLock, onToggleScreenLock: _toggleScreenLock,
onToggleFullscreen: _toggleFullscreen, onToggleFullscreen: _toggleFullscreen,
@@ -20,6 +20,7 @@ import '../../../services/file_picker_service.dart';
import '../../../services/settings_service.dart'; import '../../../services/settings_service.dart';
import '../../../services/shader_service.dart'; import '../../../services/shader_service.dart';
import '../../../services/sleep_timer_service.dart'; import '../../../services/sleep_timer_service.dart';
import '../../../services/video_filter_manager.dart';
import '../../../focus/focusable_wrapper.dart'; import '../../../focus/focusable_wrapper.dart';
import '../../../utils/dialogs.dart'; import '../../../utils/dialogs.dart';
import '../../../utils/formatters.dart'; import '../../../utils/formatters.dart';
@@ -35,7 +36,18 @@ import '../../../i18n/strings.g.dart';
import 'base_video_control_sheet.dart'; import 'base_video_control_sheet.dart';
import 'version_quality_sheet.dart'; import 'version_quality_sheet.dart';
enum _SettingsView { menu, speed, versionQuality, sleep, audioSync, subtitleSync, audioDevice, shader, dvConversion } enum _SettingsView {
menu,
speed,
zoom,
versionQuality,
sleep,
audioSync,
subtitleSync,
audioDevice,
shader,
dvConversion,
}
class _SettingsMenuItem extends StatelessWidget { class _SettingsMenuItem extends StatelessWidget {
final IconData icon; final IconData icon;
@@ -115,6 +127,9 @@ class VideoSettingsSheet extends StatefulWidget {
final Player player; final Player player;
final int audioSyncOffset; final int audioSyncOffset;
final int subtitleSyncOffset; final int subtitleSyncOffset;
final double videoZoomScale;
final ValueChanged<double>? onVideoZoomChanged;
final VoidCallback? onResetVideoZoom;
/// Whether the user can control playback (false hides speed option in host-only mode). /// Whether the user can control playback (false hides speed option in host-only mode).
final bool canControl; final bool canControl;
@@ -157,6 +172,9 @@ class VideoSettingsSheet extends StatefulWidget {
required this.player, required this.player,
required this.audioSyncOffset, required this.audioSyncOffset,
required this.subtitleSyncOffset, required this.subtitleSyncOffset,
this.videoZoomScale = 1.0,
this.onVideoZoomChanged,
this.onResetVideoZoom,
this.canControl = true, this.canControl = true,
this.isLive = false, this.isLive = false,
this.availableVersions = const [], this.availableVersions = const [],
@@ -183,6 +201,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
_SettingsView _currentView = _SettingsView.menu; _SettingsView _currentView = _SettingsView.menu;
late int _audioSyncOffset; late int _audioSyncOffset;
late int _subtitleSyncOffset; late int _subtitleSyncOffset;
late double _zoomScale;
String _dvConversionMode = 'auto'; String _dvConversionMode = 'auto';
bool get _showDebugDvConversionMode { bool get _showDebugDvConversionMode {
@@ -196,9 +215,18 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
super.initState(); super.initState();
_audioSyncOffset = widget.audioSyncOffset; _audioSyncOffset = widget.audioSyncOffset;
_subtitleSyncOffset = widget.subtitleSyncOffset; _subtitleSyncOffset = widget.subtitleSyncOffset;
_zoomScale = widget.videoZoomScale;
_loadDebugDvConversionMode(); _loadDebugDvConversionMode();
} }
@override
void didUpdateWidget(covariant VideoSettingsSheet oldWidget) {
super.didUpdateWidget(oldWidget);
if ((widget.videoZoomScale - oldWidget.videoZoomScale).abs() > 0.0001) {
_zoomScale = widget.videoZoomScale;
}
}
Future<void> _loadDebugDvConversionMode() async { Future<void> _loadDebugDvConversionMode() async {
if (!_showDebugDvConversionMode) return; if (!_showDebugDvConversionMode) return;
final dvConversionMode = await widget.player.getProperty('dv-conversion-mode'); final dvConversionMode = await widget.player.getProperty('dv-conversion-mode');
@@ -292,6 +320,8 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
return t.videoControls.settingsButton; return t.videoControls.settingsButton;
case _SettingsView.speed: case _SettingsView.speed:
return t.videoSettings.playbackSpeed; return t.videoSettings.playbackSpeed;
case _SettingsView.zoom:
return t.videoSettings.zoom;
case _SettingsView.versionQuality: case _SettingsView.versionQuality:
return _versionQualityTitle(); return _versionQualityTitle();
case _SettingsView.sleep: case _SettingsView.sleep:
@@ -315,6 +345,8 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
return Symbols.tune_rounded; return Symbols.tune_rounded;
case _SettingsView.speed: case _SettingsView.speed:
return Symbols.speed_rounded; return Symbols.speed_rounded;
case _SettingsView.zoom:
return Symbols.zoom_in_rounded;
case _SettingsView.versionQuality: case _SettingsView.versionQuality:
return Symbols.art_track; return Symbols.art_track;
case _SettingsView.sleep: case _SettingsView.sleep:
@@ -357,6 +389,28 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
return 'Active (${formatDurationWithSeconds(remaining)})'; return 'Active (${formatDurationWithSeconds(remaining)})';
} }
String _formatZoomScale(double scale) => '${(scale * 100).round()}%';
void _setZoomScale(double scale) {
final next = scale.clamp(VideoFilterManager.minZoomScale, VideoFilterManager.maxZoomScale).toDouble();
setState(() {
_zoomScale = next;
});
widget.onVideoZoomChanged?.call(next);
}
void _resetZoomScale() {
setState(() {
_zoomScale = 1.0;
});
final reset = widget.onResetVideoZoom;
if (reset != null) {
reset();
} else {
widget.onVideoZoomChanged?.call(1.0);
}
}
bool get _hasVersionQuality { bool get _hasVersionQuality {
return (widget.availableVersions.length > 1 || widget.serverSupportsTranscoding) && return (widget.availableVersions.length > 1 || widget.serverSupportsTranscoding) &&
(widget.onVersionSelected != null || widget.onQualitySelected != null); (widget.onVersionSelected != null || widget.onQualitySelected != null);
@@ -406,6 +460,15 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
}, },
), ),
if (widget.onVideoZoomChanged != null || widget.onResetVideoZoom != null)
_SettingsMenuItem(
icon: Symbols.zoom_in_rounded,
title: t.videoSettings.zoom,
valueText: _formatZoomScale(_zoomScale),
isHighlighted: (_zoomScale - 1.0).abs() > 0.0001,
onTap: () => _navigateTo(_SettingsView.zoom),
),
if (_hasVersionQuality) if (_hasVersionQuality)
_SettingsMenuItem( _SettingsMenuItem(
icon: Symbols.art_track, icon: Symbols.art_track,
@@ -633,6 +696,32 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
); );
} }
Widget _buildZoomView() {
const zoomPresets = [0.5, 0.75, 0.9, 1.0, 1.1, 1.2, 1.3, 1.5, 1.75, 2.0];
final primary = Theme.of(context).colorScheme.primary;
return ListView(
children: [
FocusableListTile(
leading: AppIcon(Symbols.restart_alt_rounded, fill: 1, color: tokens(context).textMuted),
title: Text(t.common.reset),
onTap: _resetZoomScale,
),
for (final scale in zoomPresets)
FocusableListTile(
title: Text(
_formatZoomScale(scale),
style: TextStyle(color: (_zoomScale - scale).abs() < 0.005 ? primary : null),
),
trailing: (_zoomScale - scale).abs() < 0.005
? AppIcon(Symbols.check_rounded, fill: 1, color: primary)
: null,
onTap: () => _setZoomScale(scale),
),
],
);
}
Widget _buildSleepView() { Widget _buildSleepView() {
final sleepTimer = SleepTimerService(); final sleepTimer = SleepTimerService();
@@ -894,9 +983,10 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sleepTimer = SleepTimerService(); final sleepTimer = SleepTimerService();
final isShaderActive = widget.shaderService != null && widget.shaderService!.currentPreset.isEnabled; final isShaderActive = widget.shaderService != null && widget.shaderService!.currentPreset.isEnabled;
final isZoomActive = (_zoomScale - 1.0).abs() > 0.0001;
final isIconActive = final isIconActive =
_currentView == _SettingsView.menu && _currentView == _SettingsView.menu &&
(sleepTimer.isActive || _audioSyncOffset != 0 || _subtitleSyncOffset != 0 || isShaderActive); (sleepTimer.isActive || _audioSyncOffset != 0 || _subtitleSyncOffset != 0 || isShaderActive || isZoomActive);
return BaseVideoControlSheet( return BaseVideoControlSheet(
title: _getTitle(), title: _getTitle(),
@@ -913,6 +1003,8 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
return _buildMenuView(); return _buildMenuView();
case _SettingsView.speed: case _SettingsView.speed:
return _buildSpeedView(); return _buildSpeedView();
case _SettingsView.zoom:
return _buildZoomView();
case _SettingsView.versionQuality: case _SettingsView.versionQuality:
return _buildVersionQualityView(); return _buildVersionQualityView();
case _SettingsView.sleep: case _SettingsView.sleep:
+27 -1
View File
@@ -1,7 +1,16 @@
import 'dart:async' show StreamSubscription, Timer, unawaited; import 'dart:async' show StreamSubscription, Timer, unawaited;
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:flutter/gestures.dart' show PointerSignalEvent, PointerScrollEvent; import 'package:flutter/gestures.dart'
show
PointerCancelEvent,
PointerDeviceKind,
PointerDownEvent,
PointerMoveEvent,
PointerScrollEvent,
PointerSignalEvent,
PointerUpEvent,
kDoubleTapTimeout;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart'; import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@@ -54,6 +63,7 @@ import '../../i18n/strings.g.dart';
import '../../focus/input_mode_tracker.dart'; import '../../focus/input_mode_tracker.dart';
import 'models/track_controls_state.dart'; import 'models/track_controls_state.dart';
import 'widgets/double_tap_feedback.dart'; import 'widgets/double_tap_feedback.dart';
import 'helpers/two_finger_double_tap_tracker.dart';
import 'widgets/linux_keep_alive.dart'; import 'widgets/linux_keep_alive.dart';
import 'widgets/mobile_skip_zones.dart'; import 'widgets/mobile_skip_zones.dart';
import 'widgets/skip_marker_button.dart'; import 'widgets/skip_marker_button.dart';
@@ -150,8 +160,13 @@ class PlexVideoControls extends StatefulWidget {
final int? selectedSubtitleStreamId; final int? selectedSubtitleStreamId;
final int? sourcePartId; final int? sourcePartId;
final int boxFitMode; final int boxFitMode;
final double videoZoomScale;
final VoidCallback? onTogglePIPMode; final VoidCallback? onTogglePIPMode;
final VoidCallback? onCycleBoxFitMode; final VoidCallback? onCycleBoxFitMode;
final ValueChanged<double>? onVideoZoomChanged;
final VoidCallback? onZoomIn;
final VoidCallback? onZoomOut;
final VoidCallback? onResetVideoZoom;
final VoidCallback? onCycleAudioTrack; final VoidCallback? onCycleAudioTrack;
final VoidCallback? onCycleSubtitleTrack; final VoidCallback? onCycleSubtitleTrack;
final Function(AudioTrack)? onAudioTrackChanged; final Function(AudioTrack)? onAudioTrackChanged;
@@ -247,8 +262,13 @@ class PlexVideoControls extends StatefulWidget {
this.selectedSubtitleStreamId, this.selectedSubtitleStreamId,
this.sourcePartId, this.sourcePartId,
this.boxFitMode = 0, this.boxFitMode = 0,
this.videoZoomScale = 1.0,
this.onTogglePIPMode, this.onTogglePIPMode,
this.onCycleBoxFitMode, this.onCycleBoxFitMode,
this.onVideoZoomChanged,
this.onZoomIn,
this.onZoomOut,
this.onResetVideoZoom,
this.onCycleAudioTrack, this.onCycleAudioTrack,
this.onCycleSubtitleTrack, this.onCycleSubtitleTrack,
this.onAudioTrackChanged, this.onAudioTrackChanged,
@@ -324,6 +344,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
bool _lastSkipTapWasForward = true; bool _lastSkipTapWasForward = true;
DateTime? _lastSkipActionTime; // Debounce: prevents double-tap counting as 2 skips DateTime? _lastSkipActionTime; // Debounce: prevents double-tap counting as 2 skips
Timer? _singleTapTimer; // Timer for delayed single-tap action (toggle controls) Timer? _singleTapTimer; // Timer for delayed single-tap action (toggle controls)
final TwoFingerDoubleTapTracker _twoFingerDoubleTapTracker = TwoFingerDoubleTapTracker();
DateTime? _suppressTouchTapUntil;
// Seek throttle // Seek throttle
late final Throttle _seekThrottle; late final Throttle _seekThrottle;
// Current marker state // Current marker state
@@ -553,6 +575,10 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
onKeyEvent: (node, event) => _handleControlsKeyEvent(event, isMobile), onKeyEvent: (node, event) => _handleControlsKeyEvent(event, isMobile),
child: Listener( child: Listener(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onPointerDown: isMobile ? _handleTouchPointerDown : null,
onPointerMove: isMobile ? _handleTouchPointerMove : null,
onPointerUp: isMobile ? _handleTouchPointerUp : null,
onPointerCancel: isMobile ? _handleTouchPointerCancel : null,
onPointerSignal: _handlePointerSignal, onPointerSignal: _handlePointerSignal,
child: MouseRegion( child: MouseRegion(
cursor: (_showControls || _forceShowControls) ? SystemMouseCursors.basic : SystemMouseCursors.none, cursor: (_showControls || _forceShowControls) ? SystemMouseCursors.basic : SystemMouseCursors.none,
@@ -70,6 +70,7 @@ class TrackChapterControls extends StatelessWidget {
bool get serverSupportsTranscoding => trackControlsState.serverSupportsTranscoding; bool get serverSupportsTranscoding => trackControlsState.serverSupportsTranscoding;
ValueChanged<TranscodeQualityPreset>? get onSwitchQualityPreset => trackControlsState.onSwitchQualityPreset; ValueChanged<TranscodeQualityPreset>? get onSwitchQualityPreset => trackControlsState.onSwitchQualityPreset;
int get boxFitMode => trackControlsState.boxFitMode; int get boxFitMode => trackControlsState.boxFitMode;
double get videoZoomScale => trackControlsState.videoZoomScale;
int get audioSyncOffset => trackControlsState.audioSyncOffset; int get audioSyncOffset => trackControlsState.audioSyncOffset;
int get subtitleSyncOffset => trackControlsState.subtitleSyncOffset; int get subtitleSyncOffset => trackControlsState.subtitleSyncOffset;
bool get isRotationLocked => trackControlsState.isRotationLocked; bool get isRotationLocked => trackControlsState.isRotationLocked;
@@ -78,6 +79,8 @@ class TrackChapterControls extends StatelessWidget {
bool get isAlwaysOnTop => trackControlsState.isAlwaysOnTop; bool get isAlwaysOnTop => trackControlsState.isAlwaysOnTop;
VoidCallback? get onTogglePIPMode => trackControlsState.onTogglePIPMode; VoidCallback? get onTogglePIPMode => trackControlsState.onTogglePIPMode;
VoidCallback? get onCycleBoxFitMode => trackControlsState.onCycleBoxFitMode; VoidCallback? get onCycleBoxFitMode => trackControlsState.onCycleBoxFitMode;
ValueChanged<double>? get onVideoZoomChanged => trackControlsState.onVideoZoomChanged;
VoidCallback? get onResetVideoZoom => trackControlsState.onResetVideoZoom;
VoidCallback? get onToggleRotationLock => trackControlsState.onToggleRotationLock; VoidCallback? get onToggleRotationLock => trackControlsState.onToggleRotationLock;
VoidCallback? get onToggleScreenLock => trackControlsState.onToggleScreenLock; VoidCallback? get onToggleScreenLock => trackControlsState.onToggleScreenLock;
VoidCallback? get onToggleFullscreen => trackControlsState.onToggleFullscreen; VoidCallback? get onToggleFullscreen => trackControlsState.onToggleFullscreen;
@@ -196,7 +199,13 @@ class TrackChapterControls extends StatelessWidget {
final sleepTimer = SleepTimerService(); final sleepTimer = SleepTimerService();
final isShaderActive = final isShaderActive =
shaderService != null && shaderService!.isSupported && shaderService!.currentPreset.isEnabled; shaderService != null && shaderService!.isSupported && shaderService!.currentPreset.isEnabled;
final isActive = sleepTimer.isActive || audioSyncOffset != 0 || subtitleSyncOffset != 0 || isShaderActive; final isZoomActive = (videoZoomScale - 1.0).abs() > 0.0001;
final isActive =
sleepTimer.isActive ||
audioSyncOffset != 0 ||
subtitleSyncOffset != 0 ||
isShaderActive ||
isZoomActive;
return _buildTrackButton( return _buildTrackButton(
buttonIndex: 0, buttonIndex: 0,
icon: Symbols.tune_rounded, icon: Symbols.tune_rounded,
@@ -213,6 +222,9 @@ class TrackChapterControls extends StatelessWidget {
player: player, player: player,
audioSyncOffset: audioSyncOffset, audioSyncOffset: audioSyncOffset,
subtitleSyncOffset: subtitleSyncOffset, subtitleSyncOffset: subtitleSyncOffset,
videoZoomScale: videoZoomScale,
onVideoZoomChanged: onVideoZoomChanged,
onResetVideoZoom: onResetVideoZoom,
canControl: canControl, canControl: canControl,
isLive: isLive, isLive: isLive,
availableVersions: availableVersions, availableVersions: availableVersions,
@@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/mpv/mpv.dart'; import 'package:plezy/mpv/mpv.dart';
import 'package:plezy/services/keyboard_shortcuts_service.dart'; import 'package:plezy/services/keyboard_shortcuts_service.dart';
import 'package:plezy/services/settings_service.dart'; import 'package:plezy/services/settings_service.dart';
import 'package:plezy/services/video_filter_manager.dart';
import '../test_helpers/prefs.dart'; import '../test_helpers/prefs.dart';
@@ -13,7 +14,7 @@ void main() {
SettingsService.resetForTesting(); SettingsService.resetForTesting();
}); });
testWidgets('Ctrl+S takes a screenshot and shows feedback', (tester) async { testWidgets('Ctrl+S takes a screenshot once while held', (tester) async {
final service = await KeyboardShortcutsService.getInstance(); final service = await KeyboardShortcutsService.getInstance();
addTearDown(service.dispose); addTearDown(service.dispose);
final player = _FakePlayer(); final player = _FakePlayer();
@@ -35,15 +36,197 @@ void main() {
null, null,
onScreenshot: () => feedbackCount++, onScreenshot: () => feedbackCount++,
); );
final repeatResult = service.handleVideoPlayerKeyEvent(
const KeyRepeatEvent(
physicalKey: PhysicalKeyboardKey.keyS,
logicalKey: LogicalKeyboardKey.keyS,
timeStamp: Duration(milliseconds: 30),
),
player,
null,
null,
null,
null,
null,
null,
onScreenshot: () => feedbackCount++,
);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump(); await tester.pump();
expect(result, KeyEventResult.handled); expect(result, KeyEventResult.handled);
expect(repeatResult, KeyEventResult.handled);
expect(player.commands, [ expect(player.commands, [
['screenshot', 'subtitles'], ['screenshot', 'subtitles'],
]); ]);
expect(feedbackCount, 1); expect(feedbackCount, 1);
}); });
testWidgets('Alt+Plus triggers zoom in callback', (tester) async {
final service = await KeyboardShortcutsService.getInstance();
addTearDown(service.dispose);
final player = _FakePlayer();
var zoomInCount = 0;
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
final result = service.handleVideoPlayerKeyEvent(
const KeyDownEvent(
physicalKey: PhysicalKeyboardKey.equal,
logicalKey: LogicalKeyboardKey.equal,
timeStamp: Duration.zero,
),
player,
null,
null,
null,
null,
null,
null,
onZoomIn: () => zoomInCount++,
);
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
expect(result, KeyEventResult.handled);
expect(zoomInCount, 1);
});
testWidgets('Alt+Plus repeats zoom in callback while held', (tester) async {
final service = await KeyboardShortcutsService.getInstance();
addTearDown(service.dispose);
final player = _FakePlayer();
var zoomInCount = 0;
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
final downResult = service.handleVideoPlayerKeyEvent(
const KeyDownEvent(
physicalKey: PhysicalKeyboardKey.equal,
logicalKey: LogicalKeyboardKey.equal,
timeStamp: Duration.zero,
),
player,
null,
null,
null,
null,
null,
null,
onZoomIn: () => zoomInCount++,
);
final repeatResult = service.handleVideoPlayerKeyEvent(
const KeyRepeatEvent(
physicalKey: PhysicalKeyboardKey.equal,
logicalKey: LogicalKeyboardKey.equal,
timeStamp: Duration(milliseconds: 30),
),
player,
null,
null,
null,
null,
null,
null,
onZoomIn: () => zoomInCount++,
);
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
expect(downResult, KeyEventResult.handled);
expect(repeatResult, KeyEventResult.handled);
expect(zoomInCount, 2);
});
testWidgets('Alt+Minus repeats zoom out callback while held', (tester) async {
final service = await KeyboardShortcutsService.getInstance();
addTearDown(service.dispose);
final player = _FakePlayer();
var zoomOutCount = 0;
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
final downResult = service.handleVideoPlayerKeyEvent(
const KeyDownEvent(
physicalKey: PhysicalKeyboardKey.minus,
logicalKey: LogicalKeyboardKey.minus,
timeStamp: Duration.zero,
),
player,
null,
null,
null,
null,
null,
null,
onZoomOut: () => zoomOutCount++,
);
final repeatResult = service.handleVideoPlayerKeyEvent(
const KeyRepeatEvent(
physicalKey: PhysicalKeyboardKey.minus,
logicalKey: LogicalKeyboardKey.minus,
timeStamp: Duration(milliseconds: 30),
),
player,
null,
null,
null,
null,
null,
null,
onZoomOut: () => zoomOutCount++,
);
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
expect(downResult, KeyEventResult.handled);
expect(repeatResult, KeyEventResult.handled);
expect(zoomOutCount, 2);
});
testWidgets('Alt+Backspace reset does not repeat while held', (tester) async {
final service = await KeyboardShortcutsService.getInstance();
addTearDown(service.dispose);
final player = _FakePlayer();
var resetCount = 0;
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
final downResult = service.handleVideoPlayerKeyEvent(
const KeyDownEvent(
physicalKey: PhysicalKeyboardKey.backspace,
logicalKey: LogicalKeyboardKey.backspace,
timeStamp: Duration.zero,
),
player,
null,
null,
null,
null,
null,
null,
onZoomReset: () => resetCount++,
);
final repeatResult = service.handleVideoPlayerKeyEvent(
const KeyRepeatEvent(
physicalKey: PhysicalKeyboardKey.backspace,
logicalKey: LogicalKeyboardKey.backspace,
timeStamp: Duration(milliseconds: 30),
),
player,
null,
null,
null,
null,
null,
null,
onZoomReset: () => resetCount++,
);
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
expect(downResult, KeyEventResult.handled);
expect(repeatResult, KeyEventResult.handled);
expect(resetCount, 1);
});
test('video zoom scale maps to mpv logarithmic property', () {
expect(VideoFilterManager.videoZoomPropertyForScale(1.0), closeTo(0.0, 0.0001));
expect(VideoFilterManager.videoZoomPropertyForScale(2.0), closeTo(1.0, 0.0001));
expect(VideoFilterManager.videoZoomPropertyForScale(0.5), closeTo(-1.0, 0.0001));
});
} }
class _FakePlayer implements Player { class _FakePlayer implements Player {
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/widgets/video_controls/helpers/two_finger_double_tap_tracker.dart';
void main() {
late DateTime now;
late TwoFingerDoubleTapTracker tracker;
void advance(Duration duration) {
now = now.add(duration);
}
setUp(() {
now = DateTime(2026);
tracker = TwoFingerDoubleTapTracker(now: () => now);
});
test('detects two two-finger taps as double tap', () {
tracker.pointerDown(1, const Offset(100, 100));
tracker.pointerDown(2, const Offset(140, 100));
expect(tracker.pointerUp(1, const Offset(100, 100)), isFalse);
expect(tracker.pointerUp(2, const Offset(140, 100)), isFalse);
advance(const Duration(milliseconds: 120));
tracker.pointerDown(3, const Offset(102, 102));
tracker.pointerDown(4, const Offset(142, 102));
expect(tracker.pointerUp(3, const Offset(102, 102)), isFalse);
expect(tracker.pointerUp(4, const Offset(142, 102)), isTrue);
});
test('does not detect one-finger double tap', () {
tracker.pointerDown(1, const Offset(100, 100));
expect(tracker.pointerUp(1, const Offset(100, 100)), isFalse);
advance(const Duration(milliseconds: 120));
tracker.pointerDown(2, const Offset(100, 100));
expect(tracker.pointerUp(2, const Offset(100, 100)), isFalse);
});
test('movement invalidates a candidate two-finger tap', () {
tracker.pointerDown(1, const Offset(100, 100));
tracker.pointerDown(2, const Offset(140, 100));
tracker.pointerMove(1, const Offset(140, 140));
expect(tracker.pointerUp(1, const Offset(140, 140)), isFalse);
expect(tracker.pointerUp(2, const Offset(140, 100)), isFalse);
advance(const Duration(milliseconds: 120));
tracker.pointerDown(3, const Offset(100, 100));
tracker.pointerDown(4, const Offset(140, 100));
expect(tracker.pointerUp(3, const Offset(100, 100)), isFalse);
expect(tracker.pointerUp(4, const Offset(140, 100)), isFalse);
});
test('third finger invalidates a candidate two-finger tap', () {
tracker.pointerDown(1, const Offset(100, 100));
tracker.pointerDown(2, const Offset(140, 100));
tracker.pointerDown(3, const Offset(180, 100));
expect(tracker.pointerUp(1, const Offset(100, 100)), isFalse);
expect(tracker.pointerUp(2, const Offset(140, 100)), isFalse);
expect(tracker.pointerUp(3, const Offset(180, 100)), isFalse);
advance(const Duration(milliseconds: 120));
tracker.pointerDown(4, const Offset(100, 100));
tracker.pointerDown(5, const Offset(140, 100));
expect(tracker.pointerUp(4, const Offset(100, 100)), isFalse);
expect(tracker.pointerUp(5, const Offset(140, 100)), isFalse);
});
}