@@ -320,6 +320,9 @@
|
||||
"versionsButton": "Videoversionen",
|
||||
"pipButton": "Bild-in-Bild Modus",
|
||||
"aspectRatioButton": "Seitenverhältnis",
|
||||
"ambientLighting": "Umgebungsbeleuchtung",
|
||||
"ambientLightingOn": "Umgebungsbeleuchtung aktivieren",
|
||||
"ambientLightingOff": "Umgebungsbeleuchtung deaktivieren",
|
||||
"fullscreenButton": "Vollbild aktivieren",
|
||||
"exitFullscreenButton": "Vollbild verlassen",
|
||||
"alwaysOnTopButton": "Immer im Vordergrund",
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
"versionsButton": "Video versions",
|
||||
"pipButton": "Picture-in-Picture mode",
|
||||
"aspectRatioButton": "Aspect ratio",
|
||||
"ambientLighting": "Ambient lighting",
|
||||
"ambientLightingOn": "Enable ambient lighting",
|
||||
"ambientLightingOff": "Disable ambient lighting",
|
||||
"fullscreenButton": "Enter fullscreen",
|
||||
"exitFullscreenButton": "Exit fullscreen",
|
||||
"alwaysOnTopButton": "Always on top",
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
"versionsButton": "Versiones de video",
|
||||
"pipButton": "Modo PiP (Imagen en Imagen)",
|
||||
"aspectRatioButton": "Relación de aspecto",
|
||||
"ambientLighting": "Iluminación ambiental",
|
||||
"ambientLightingOn": "Activar iluminación ambiental",
|
||||
"ambientLightingOff": "Desactivar iluminación ambiental",
|
||||
"fullscreenButton": "Entrar en pantalla completa",
|
||||
"exitFullscreenButton": "Salir de pantalla completa",
|
||||
"alwaysOnTopButton": "Siempre visible",
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
"versionsButton": "Versions vidéo",
|
||||
"pipButton": "Mode PiP (Picture-in-Picture)",
|
||||
"aspectRatioButton": "Format d'image",
|
||||
"ambientLighting": "Éclairage ambiant",
|
||||
"ambientLightingOn": "Activer l'éclairage ambiant",
|
||||
"ambientLightingOff": "Désactiver l'éclairage ambiant",
|
||||
"fullscreenButton": "Passer en mode plein écran",
|
||||
"exitFullscreenButton": "Quitter le mode plein écran",
|
||||
"alwaysOnTopButton": "Always on top",
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
"versionsButton": "Versioni video",
|
||||
"pipButton": "Modalità Picture-in-Picture",
|
||||
"aspectRatioButton": "Proporzioni",
|
||||
"ambientLighting": "Illuminazione ambientale",
|
||||
"ambientLightingOn": "Attiva illuminazione ambientale",
|
||||
"ambientLightingOff": "Disattiva illuminazione ambientale",
|
||||
"fullscreenButton": "Attiva schermo intero",
|
||||
"exitFullscreenButton": "Esci da schermo intero",
|
||||
"alwaysOnTopButton": "Sempre in primo piano",
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
"versionsButton": "동영상 버전",
|
||||
"pipButton": "픽처 인 픽처 모드",
|
||||
"aspectRatioButton": "화면비율",
|
||||
"ambientLighting": "주변 조명",
|
||||
"ambientLightingOn": "주변 조명 켜기",
|
||||
"ambientLightingOff": "주변 조명 끄기",
|
||||
"fullscreenButton": "전체화면",
|
||||
"exitFullscreenButton": "전체화면 종료",
|
||||
"alwaysOnTopButton": "창 최상위 고정",
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
"versionsButton": "Videoversies",
|
||||
"pipButton": "Beeld-in-beeld modus",
|
||||
"aspectRatioButton": "Beeldverhouding",
|
||||
"ambientLighting": "Omgevingsverlichting",
|
||||
"ambientLightingOn": "Omgevingsverlichting inschakelen",
|
||||
"ambientLightingOff": "Omgevingsverlichting uitschakelen",
|
||||
"fullscreenButton": "Volledig scherm activeren",
|
||||
"exitFullscreenButton": "Volledig scherm verlaten",
|
||||
"alwaysOnTopButton": "Altijd bovenop",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 9
|
||||
/// Strings: 6642 (738 per locale)
|
||||
/// Strings: 6669 (741 per locale)
|
||||
///
|
||||
/// Built on 2026-02-16 at 00:20 UTC
|
||||
/// Built on 2026-02-18 at 03:36 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -474,6 +474,9 @@ class _TranslationsVideoControlsDe implements TranslationsVideoControlsEn {
|
||||
@override String get versionsButton => 'Videoversionen';
|
||||
@override String get pipButton => 'Bild-in-Bild Modus';
|
||||
@override String get aspectRatioButton => 'Seitenverhältnis';
|
||||
@override String get ambientLighting => 'Umgebungsbeleuchtung';
|
||||
@override String get ambientLightingOn => 'Umgebungsbeleuchtung aktivieren';
|
||||
@override String get ambientLightingOff => 'Umgebungsbeleuchtung deaktivieren';
|
||||
@override String get fullscreenButton => 'Vollbild aktivieren';
|
||||
@override String get exitFullscreenButton => 'Vollbild verlassen';
|
||||
@override String get alwaysOnTopButton => 'Immer im Vordergrund';
|
||||
@@ -1517,6 +1520,9 @@ extension on TranslationsDe {
|
||||
'videoControls.versionsButton' => 'Videoversionen',
|
||||
'videoControls.pipButton' => 'Bild-in-Bild Modus',
|
||||
'videoControls.aspectRatioButton' => 'Seitenverhältnis',
|
||||
'videoControls.ambientLighting' => 'Umgebungsbeleuchtung',
|
||||
'videoControls.ambientLightingOn' => 'Umgebungsbeleuchtung aktivieren',
|
||||
'videoControls.ambientLightingOff' => 'Umgebungsbeleuchtung deaktivieren',
|
||||
'videoControls.fullscreenButton' => 'Vollbild aktivieren',
|
||||
'videoControls.exitFullscreenButton' => 'Vollbild verlassen',
|
||||
'videoControls.alwaysOnTopButton' => 'Immer im Vordergrund',
|
||||
@@ -1734,11 +1740,11 @@ extension on TranslationsDe {
|
||||
'liveTv.overnight' => 'Nacht',
|
||||
'liveTv.morning' => 'Morgen',
|
||||
'liveTv.daytime' => 'Tagsüber',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => 'Abend',
|
||||
'liveTv.lateNight' => 'Spätnacht',
|
||||
'liveTv.whatsOn' => 'Jetzt im TV',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => 'Kanal ansehen',
|
||||
'downloads.title' => 'Downloads',
|
||||
'downloads.manage' => 'Verwalten',
|
||||
|
||||
@@ -1025,6 +1025,15 @@ class TranslationsVideoControlsEn {
|
||||
/// en: 'Aspect ratio'
|
||||
String get aspectRatioButton => 'Aspect ratio';
|
||||
|
||||
/// en: 'Ambient lighting'
|
||||
String get ambientLighting => 'Ambient lighting';
|
||||
|
||||
/// en: 'Enable ambient lighting'
|
||||
String get ambientLightingOn => 'Enable ambient lighting';
|
||||
|
||||
/// en: 'Disable ambient lighting'
|
||||
String get ambientLightingOff => 'Disable ambient lighting';
|
||||
|
||||
/// en: 'Enter fullscreen'
|
||||
String get fullscreenButton => 'Enter fullscreen';
|
||||
|
||||
@@ -3000,6 +3009,9 @@ extension on Translations {
|
||||
'videoControls.versionsButton' => 'Video versions',
|
||||
'videoControls.pipButton' => 'Picture-in-Picture mode',
|
||||
'videoControls.aspectRatioButton' => 'Aspect ratio',
|
||||
'videoControls.ambientLighting' => 'Ambient lighting',
|
||||
'videoControls.ambientLightingOn' => 'Enable ambient lighting',
|
||||
'videoControls.ambientLightingOff' => 'Disable ambient lighting',
|
||||
'videoControls.fullscreenButton' => 'Enter fullscreen',
|
||||
'videoControls.exitFullscreenButton' => 'Exit fullscreen',
|
||||
'videoControls.alwaysOnTopButton' => 'Always on top',
|
||||
@@ -3217,11 +3229,11 @@ extension on Translations {
|
||||
'liveTv.overnight' => 'Overnight',
|
||||
'liveTv.morning' => 'Morning',
|
||||
'liveTv.daytime' => 'Daytime',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => 'Evening',
|
||||
'liveTv.lateNight' => 'Late Night',
|
||||
'liveTv.whatsOn' => 'What\'s On',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => 'Watch Channel',
|
||||
'collections.title' => 'Collections',
|
||||
'collections.collection' => 'Collection',
|
||||
|
||||
@@ -474,6 +474,9 @@ class _TranslationsVideoControlsEs implements TranslationsVideoControlsEn {
|
||||
@override String get versionsButton => 'Versiones de video';
|
||||
@override String get pipButton => 'Modo PiP (Imagen en Imagen)';
|
||||
@override String get aspectRatioButton => 'Relación de aspecto';
|
||||
@override String get ambientLighting => 'Iluminación ambiental';
|
||||
@override String get ambientLightingOn => 'Activar iluminación ambiental';
|
||||
@override String get ambientLightingOff => 'Desactivar iluminación ambiental';
|
||||
@override String get fullscreenButton => 'Entrar en pantalla completa';
|
||||
@override String get exitFullscreenButton => 'Salir de pantalla completa';
|
||||
@override String get alwaysOnTopButton => 'Siempre visible';
|
||||
@@ -1517,6 +1520,9 @@ extension on TranslationsEs {
|
||||
'videoControls.versionsButton' => 'Versiones de video',
|
||||
'videoControls.pipButton' => 'Modo PiP (Imagen en Imagen)',
|
||||
'videoControls.aspectRatioButton' => 'Relación de aspecto',
|
||||
'videoControls.ambientLighting' => 'Iluminación ambiental',
|
||||
'videoControls.ambientLightingOn' => 'Activar iluminación ambiental',
|
||||
'videoControls.ambientLightingOff' => 'Desactivar iluminación ambiental',
|
||||
'videoControls.fullscreenButton' => 'Entrar en pantalla completa',
|
||||
'videoControls.exitFullscreenButton' => 'Salir de pantalla completa',
|
||||
'videoControls.alwaysOnTopButton' => 'Siempre visible',
|
||||
@@ -1734,11 +1740,11 @@ extension on TranslationsEs {
|
||||
'liveTv.overnight' => 'Madrugada',
|
||||
'liveTv.morning' => 'Mañana',
|
||||
'liveTv.daytime' => 'Día',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => 'Noche',
|
||||
'liveTv.lateNight' => 'Trasnoche',
|
||||
'liveTv.whatsOn' => 'En emisión',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => 'Ver canal',
|
||||
'collections.title' => 'Colecciones',
|
||||
'collections.collection' => 'Colección',
|
||||
|
||||
@@ -474,6 +474,9 @@ class _TranslationsVideoControlsFr implements TranslationsVideoControlsEn {
|
||||
@override String get versionsButton => 'Versions vidéo';
|
||||
@override String get pipButton => 'Mode PiP (Picture-in-Picture)';
|
||||
@override String get aspectRatioButton => 'Format d\'image';
|
||||
@override String get ambientLighting => 'Éclairage ambiant';
|
||||
@override String get ambientLightingOn => 'Activer l\'éclairage ambiant';
|
||||
@override String get ambientLightingOff => 'Désactiver l\'éclairage ambiant';
|
||||
@override String get fullscreenButton => 'Passer en mode plein écran';
|
||||
@override String get exitFullscreenButton => 'Quitter le mode plein écran';
|
||||
@override String get alwaysOnTopButton => 'Always on top';
|
||||
@@ -1517,6 +1520,9 @@ extension on TranslationsFr {
|
||||
'videoControls.versionsButton' => 'Versions vidéo',
|
||||
'videoControls.pipButton' => 'Mode PiP (Picture-in-Picture)',
|
||||
'videoControls.aspectRatioButton' => 'Format d\'image',
|
||||
'videoControls.ambientLighting' => 'Éclairage ambiant',
|
||||
'videoControls.ambientLightingOn' => 'Activer l\'éclairage ambiant',
|
||||
'videoControls.ambientLightingOff' => 'Désactiver l\'éclairage ambiant',
|
||||
'videoControls.fullscreenButton' => 'Passer en mode plein écran',
|
||||
'videoControls.exitFullscreenButton' => 'Quitter le mode plein écran',
|
||||
'videoControls.alwaysOnTopButton' => 'Always on top',
|
||||
@@ -1734,11 +1740,11 @@ extension on TranslationsFr {
|
||||
'liveTv.overnight' => 'Nuit',
|
||||
'liveTv.morning' => 'Matin',
|
||||
'liveTv.daytime' => 'Journée',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => 'Soirée',
|
||||
'liveTv.lateNight' => 'Nuit tardive',
|
||||
'liveTv.whatsOn' => 'En ce moment',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => 'Regarder la chaîne',
|
||||
'collections.title' => 'Collections',
|
||||
'collections.collection' => 'Collection',
|
||||
|
||||
@@ -474,6 +474,9 @@ class _TranslationsVideoControlsIt implements TranslationsVideoControlsEn {
|
||||
@override String get versionsButton => 'Versioni video';
|
||||
@override String get pipButton => 'Modalità Picture-in-Picture';
|
||||
@override String get aspectRatioButton => 'Proporzioni';
|
||||
@override String get ambientLighting => 'Illuminazione ambientale';
|
||||
@override String get ambientLightingOn => 'Attiva illuminazione ambientale';
|
||||
@override String get ambientLightingOff => 'Disattiva illuminazione ambientale';
|
||||
@override String get fullscreenButton => 'Attiva schermo intero';
|
||||
@override String get exitFullscreenButton => 'Esci da schermo intero';
|
||||
@override String get alwaysOnTopButton => 'Sempre in primo piano';
|
||||
@@ -1517,6 +1520,9 @@ extension on TranslationsIt {
|
||||
'videoControls.versionsButton' => 'Versioni video',
|
||||
'videoControls.pipButton' => 'Modalità Picture-in-Picture',
|
||||
'videoControls.aspectRatioButton' => 'Proporzioni',
|
||||
'videoControls.ambientLighting' => 'Illuminazione ambientale',
|
||||
'videoControls.ambientLightingOn' => 'Attiva illuminazione ambientale',
|
||||
'videoControls.ambientLightingOff' => 'Disattiva illuminazione ambientale',
|
||||
'videoControls.fullscreenButton' => 'Attiva schermo intero',
|
||||
'videoControls.exitFullscreenButton' => 'Esci da schermo intero',
|
||||
'videoControls.alwaysOnTopButton' => 'Sempre in primo piano',
|
||||
@@ -1734,11 +1740,11 @@ extension on TranslationsIt {
|
||||
'liveTv.overnight' => 'Notte',
|
||||
'liveTv.morning' => 'Mattina',
|
||||
'liveTv.daytime' => 'Giorno',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => 'Sera',
|
||||
'liveTv.lateNight' => 'Notte tarda',
|
||||
'liveTv.whatsOn' => 'In onda ora',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => 'Guarda canale',
|
||||
'downloads.title' => 'Download',
|
||||
'downloads.manage' => 'Gestisci',
|
||||
|
||||
@@ -474,6 +474,9 @@ class _TranslationsVideoControlsKo implements TranslationsVideoControlsEn {
|
||||
@override String get versionsButton => '동영상 버전';
|
||||
@override String get pipButton => '픽처 인 픽처 모드';
|
||||
@override String get aspectRatioButton => '화면비율';
|
||||
@override String get ambientLighting => '주변 조명';
|
||||
@override String get ambientLightingOn => '주변 조명 켜기';
|
||||
@override String get ambientLightingOff => '주변 조명 끄기';
|
||||
@override String get fullscreenButton => '전체화면';
|
||||
@override String get exitFullscreenButton => '전체화면 종료';
|
||||
@override String get alwaysOnTopButton => '창 최상위 고정';
|
||||
@@ -1517,6 +1520,9 @@ extension on TranslationsKo {
|
||||
'videoControls.versionsButton' => '동영상 버전',
|
||||
'videoControls.pipButton' => '픽처 인 픽처 모드',
|
||||
'videoControls.aspectRatioButton' => '화면비율',
|
||||
'videoControls.ambientLighting' => '주변 조명',
|
||||
'videoControls.ambientLightingOn' => '주변 조명 켜기',
|
||||
'videoControls.ambientLightingOff' => '주변 조명 끄기',
|
||||
'videoControls.fullscreenButton' => '전체화면',
|
||||
'videoControls.exitFullscreenButton' => '전체화면 종료',
|
||||
'videoControls.alwaysOnTopButton' => '창 최상위 고정',
|
||||
@@ -1734,11 +1740,11 @@ extension on TranslationsKo {
|
||||
'liveTv.overnight' => '심야',
|
||||
'liveTv.morning' => '아침',
|
||||
'liveTv.daytime' => '낮',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => '저녁',
|
||||
'liveTv.lateNight' => '심야 방송',
|
||||
'liveTv.whatsOn' => '지금 방송 중',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => '채널 시청',
|
||||
'collections.title' => '컬렉션',
|
||||
'collections.collection' => '컬렉션',
|
||||
|
||||
@@ -474,6 +474,9 @@ class _TranslationsVideoControlsNl implements TranslationsVideoControlsEn {
|
||||
@override String get versionsButton => 'Videoversies';
|
||||
@override String get pipButton => 'Beeld-in-beeld modus';
|
||||
@override String get aspectRatioButton => 'Beeldverhouding';
|
||||
@override String get ambientLighting => 'Omgevingsverlichting';
|
||||
@override String get ambientLightingOn => 'Omgevingsverlichting inschakelen';
|
||||
@override String get ambientLightingOff => 'Omgevingsverlichting uitschakelen';
|
||||
@override String get fullscreenButton => 'Volledig scherm activeren';
|
||||
@override String get exitFullscreenButton => 'Volledig scherm verlaten';
|
||||
@override String get alwaysOnTopButton => 'Altijd bovenop';
|
||||
@@ -1517,6 +1520,9 @@ extension on TranslationsNl {
|
||||
'videoControls.versionsButton' => 'Videoversies',
|
||||
'videoControls.pipButton' => 'Beeld-in-beeld modus',
|
||||
'videoControls.aspectRatioButton' => 'Beeldverhouding',
|
||||
'videoControls.ambientLighting' => 'Omgevingsverlichting',
|
||||
'videoControls.ambientLightingOn' => 'Omgevingsverlichting inschakelen',
|
||||
'videoControls.ambientLightingOff' => 'Omgevingsverlichting uitschakelen',
|
||||
'videoControls.fullscreenButton' => 'Volledig scherm activeren',
|
||||
'videoControls.exitFullscreenButton' => 'Volledig scherm verlaten',
|
||||
'videoControls.alwaysOnTopButton' => 'Altijd bovenop',
|
||||
@@ -1734,11 +1740,11 @@ extension on TranslationsNl {
|
||||
'liveTv.overnight' => 'Nacht',
|
||||
'liveTv.morning' => 'Ochtend',
|
||||
'liveTv.daytime' => 'Overdag',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => 'Avond',
|
||||
'liveTv.lateNight' => 'Late avond',
|
||||
'liveTv.whatsOn' => 'Nu op TV',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => 'Kanaal bekijken',
|
||||
'downloads.title' => 'Downloads',
|
||||
'downloads.manage' => 'Beheren',
|
||||
|
||||
@@ -474,6 +474,9 @@ class _TranslationsVideoControlsSv implements TranslationsVideoControlsEn {
|
||||
@override String get versionsButton => 'Videoversioner';
|
||||
@override String get pipButton => 'Bild-i-bild läge';
|
||||
@override String get aspectRatioButton => 'Bildförhållande';
|
||||
@override String get ambientLighting => 'Ambientbelysning';
|
||||
@override String get ambientLightingOn => 'Aktivera ambientbelysning';
|
||||
@override String get ambientLightingOff => 'Inaktivera ambientbelysning';
|
||||
@override String get fullscreenButton => 'Aktivera helskärm';
|
||||
@override String get exitFullscreenButton => 'Avsluta helskärm';
|
||||
@override String get alwaysOnTopButton => 'Alltid överst';
|
||||
@@ -1517,6 +1520,9 @@ extension on TranslationsSv {
|
||||
'videoControls.versionsButton' => 'Videoversioner',
|
||||
'videoControls.pipButton' => 'Bild-i-bild läge',
|
||||
'videoControls.aspectRatioButton' => 'Bildförhållande',
|
||||
'videoControls.ambientLighting' => 'Ambientbelysning',
|
||||
'videoControls.ambientLightingOn' => 'Aktivera ambientbelysning',
|
||||
'videoControls.ambientLightingOff' => 'Inaktivera ambientbelysning',
|
||||
'videoControls.fullscreenButton' => 'Aktivera helskärm',
|
||||
'videoControls.exitFullscreenButton' => 'Avsluta helskärm',
|
||||
'videoControls.alwaysOnTopButton' => 'Alltid överst',
|
||||
@@ -1734,11 +1740,11 @@ extension on TranslationsSv {
|
||||
'liveTv.overnight' => 'Natt',
|
||||
'liveTv.morning' => 'Morgon',
|
||||
'liveTv.daytime' => 'Dagtid',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => 'Kväll',
|
||||
'liveTv.lateNight' => 'Sen kväll',
|
||||
'liveTv.whatsOn' => 'På TV nu',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => 'Titta på kanal',
|
||||
'downloads.title' => 'Nedladdningar',
|
||||
'downloads.manage' => 'Hantera',
|
||||
|
||||
@@ -474,6 +474,9 @@ class _TranslationsVideoControlsZh implements TranslationsVideoControlsEn {
|
||||
@override String get versionsButton => '视频版本';
|
||||
@override String get pipButton => '画中画模式';
|
||||
@override String get aspectRatioButton => '宽高比';
|
||||
@override String get ambientLighting => '氛围灯光';
|
||||
@override String get ambientLightingOn => '开启氛围灯光';
|
||||
@override String get ambientLightingOff => '关闭氛围灯光';
|
||||
@override String get fullscreenButton => '进入全屏';
|
||||
@override String get exitFullscreenButton => '退出全屏';
|
||||
@override String get alwaysOnTopButton => '置顶窗口';
|
||||
@@ -1517,6 +1520,9 @@ extension on TranslationsZh {
|
||||
'videoControls.versionsButton' => '视频版本',
|
||||
'videoControls.pipButton' => '画中画模式',
|
||||
'videoControls.aspectRatioButton' => '宽高比',
|
||||
'videoControls.ambientLighting' => '氛围灯光',
|
||||
'videoControls.ambientLightingOn' => '开启氛围灯光',
|
||||
'videoControls.ambientLightingOff' => '关闭氛围灯光',
|
||||
'videoControls.fullscreenButton' => '进入全屏',
|
||||
'videoControls.exitFullscreenButton' => '退出全屏',
|
||||
'videoControls.alwaysOnTopButton' => '置顶窗口',
|
||||
@@ -1734,11 +1740,11 @@ extension on TranslationsZh {
|
||||
'liveTv.overnight' => '凌晨',
|
||||
'liveTv.morning' => '上午',
|
||||
'liveTv.daytime' => '白天',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.evening' => '晚上',
|
||||
'liveTv.lateNight' => '深夜',
|
||||
'liveTv.whatsOn' => '正在播出',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.watchChannel' => '观看频道',
|
||||
'downloads.title' => '下载',
|
||||
'downloads.manage' => '管理',
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
"versionsButton": "Videoversioner",
|
||||
"pipButton": "Bild-i-bild läge",
|
||||
"aspectRatioButton": "Bildförhållande",
|
||||
"ambientLighting": "Ambientbelysning",
|
||||
"ambientLightingOn": "Aktivera ambientbelysning",
|
||||
"ambientLightingOff": "Inaktivera ambientbelysning",
|
||||
"fullscreenButton": "Aktivera helskärm",
|
||||
"exitFullscreenButton": "Avsluta helskärm",
|
||||
"alwaysOnTopButton": "Alltid överst",
|
||||
|
||||
@@ -320,6 +320,9 @@
|
||||
"versionsButton": "视频版本",
|
||||
"pipButton": "画中画模式",
|
||||
"aspectRatioButton": "宽高比",
|
||||
"ambientLighting": "氛围灯光",
|
||||
"ambientLightingOn": "开启氛围灯光",
|
||||
"ambientLightingOff": "关闭氛围灯光",
|
||||
"fullscreenButton": "进入全屏",
|
||||
"exitFullscreenButton": "退出全屏",
|
||||
"alwaysOnTopButton": "置顶窗口",
|
||||
|
||||
@@ -36,6 +36,7 @@ import '../services/offline_watch_sync_service.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../services/sleep_timer_service.dart';
|
||||
import '../services/track_selection_service.dart';
|
||||
import '../services/ambient_lighting_service.dart';
|
||||
import '../services/video_filter_manager.dart';
|
||||
import '../services/video_pip_manager.dart';
|
||||
import '../services/pip_service.dart';
|
||||
@@ -167,6 +168,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
VideoFilterManager? _videoFilterManager;
|
||||
VideoPIPManager? _videoPIPManager;
|
||||
ShaderService? _shaderService;
|
||||
AmbientLightingService? _ambientLightingService;
|
||||
final EpisodeNavigationService _episodeNavigation = EpisodeNavigationService();
|
||||
|
||||
// Watch Together provider reference (stored early to use in dispose)
|
||||
@@ -1023,6 +1025,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
// Shader Service (MPV only)
|
||||
_shaderService = ShaderService(player!);
|
||||
if (_shaderService!.isSupported) {
|
||||
// Ambient Lighting Service
|
||||
_ambientLightingService = AmbientLightingService(player!);
|
||||
_shaderService!.ambientLightingService = _ambientLightingService;
|
||||
_videoFilterManager?.ambientLightingService = _ambientLightingService;
|
||||
|
||||
await _applySavedShaderPreset();
|
||||
}
|
||||
}
|
||||
@@ -1141,11 +1148,56 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
|
||||
/// Cycle through BoxFit modes: contain → cover → fill → contain (for button)
|
||||
void _cycleBoxFitMode() {
|
||||
// Disable ambient lighting when switching boxfit modes
|
||||
// (cover/fill change the video rect, making the baked-in shader incorrect)
|
||||
_ambientLightingService?.disable();
|
||||
setState(() {
|
||||
_videoFilterManager?.cycleBoxFitMode();
|
||||
});
|
||||
}
|
||||
|
||||
/// Update video-aspect-override when player size changes.
|
||||
/// The shader adapts automatically via built-in target_size uniform.
|
||||
void _updateAmbientLightingOnResize(Size newSize) {
|
||||
final ambientLighting = _ambientLightingService;
|
||||
if (ambientLighting == null || !ambientLighting.isEnabled) return;
|
||||
if (newSize.height == 0) return;
|
||||
|
||||
ambientLighting.updateOutputAspect(newSize.width / newSize.height);
|
||||
}
|
||||
|
||||
/// Toggle ambient lighting effect on/off
|
||||
Future<void> _toggleAmbientLighting() async {
|
||||
final ambientLighting = _ambientLightingService;
|
||||
if (ambientLighting == null || !ambientLighting.isSupported) return;
|
||||
|
||||
if (ambientLighting.isEnabled) {
|
||||
await ambientLighting.disable();
|
||||
_videoFilterManager?.updateVideoFilter();
|
||||
} else {
|
||||
// Get video display aspect ratio
|
||||
final dwidth = await player?.getProperty('dwidth');
|
||||
final dheight = await player?.getProperty('dheight');
|
||||
if (dwidth == null || dheight == null) return;
|
||||
final w = double.tryParse(dwidth);
|
||||
final h = double.tryParse(dheight);
|
||||
if (w == null || h == null || h == 0) return;
|
||||
final videoAspect = w / h;
|
||||
|
||||
// Get player widget aspect ratio
|
||||
final playerSize = _videoFilterManager?.playerSize;
|
||||
if (playerSize == null || playerSize.height == 0) return;
|
||||
final outputAspect = playerSize.width / playerSize.height;
|
||||
|
||||
// Force contain mode when enabling ambient lighting
|
||||
_videoFilterManager?.resetToContain();
|
||||
|
||||
await ambientLighting.enable(videoAspect, outputAspect);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// Toggle between contain and cover modes only (for pinch gesture)
|
||||
void _toggleContainCover() {
|
||||
setState(() {
|
||||
@@ -2151,9 +2203,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
}
|
||||
}
|
||||
}
|
||||
return event.logicalKey.isNavigationKey
|
||||
? KeyEventResult.handled
|
||||
: KeyEventResult.ignored;
|
||||
return event.logicalKey.isNavigationKey ? KeyEventResult.handled : KeyEventResult.ignored;
|
||||
},
|
||||
child: _isPlayerInitialized && player != null ? _buildVideoPlayer(context) : _buildLoadingSpinner(),
|
||||
);
|
||||
@@ -2217,6 +2267,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
if (mounted && player != null) {
|
||||
_videoFilterManager?.updatePlayerSize(newSize);
|
||||
_videoPIPManager?.updatePlayerSize(newSize);
|
||||
// Update ambient lighting shader if active (output aspect changed)
|
||||
_updateAmbientLightingOnResize(newSize);
|
||||
// Update Metal layer frame on iOS/macOS for rotation
|
||||
player!.updateFrame();
|
||||
}
|
||||
@@ -2286,6 +2338,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
: null,
|
||||
isLive: widget.isLive,
|
||||
liveChannelName: _liveChannelName,
|
||||
isAmbientLightingEnabled: _ambientLightingService?.isEnabled ?? false,
|
||||
onToggleAmbientLighting: _toggleAmbientLighting,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../mpv/player/player.dart';
|
||||
|
||||
/// Generates and manages an ambient lighting GLSL shader that fills letterbox/pillarbox
|
||||
/// bars with a blurred, dimmed version of the video edges.
|
||||
///
|
||||
/// Uses video-aspect-override to fill the window (eliminating black bars), then
|
||||
/// a GLSL shader composites the sharp original video centered at correct aspect
|
||||
/// over a blurred background.
|
||||
///
|
||||
/// The shader uses MPV's built-in `input_size` and `target_size` uniforms to
|
||||
/// dynamically compute the video rect position. On window resize, only
|
||||
/// `video-aspect-override` needs updating — no shader regeneration required.
|
||||
class AmbientLightingService {
|
||||
final Player _player;
|
||||
String? _shaderPath;
|
||||
bool _enabled = false;
|
||||
|
||||
/// Brightness multiplier for the blurred background (0.0-1.0).
|
||||
static const double _brightness = 0.5;
|
||||
|
||||
AmbientLightingService(this._player);
|
||||
|
||||
bool get isEnabled => _enabled;
|
||||
bool get isSupported => _player.playerType == 'mpv';
|
||||
|
||||
/// Enable ambient lighting effect.
|
||||
///
|
||||
/// [videoAspect] - the source video's display aspect ratio (width/height).
|
||||
/// [outputAspect] - the player widget's aspect ratio (width/height).
|
||||
Future<void> enable(double videoAspect, double outputAspect) async {
|
||||
if (!isSupported) return;
|
||||
|
||||
try {
|
||||
// Write static shader (only needs to happen once)
|
||||
if (_shaderPath == null) {
|
||||
_shaderPath = await _writeShaderToTemp(_generateShader());
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('AmbientLightingService: Shader path: $_shaderPath');
|
||||
}
|
||||
|
||||
// Set video-aspect-override to fill the entire output area
|
||||
await _player.setProperty('video-aspect-override', outputAspect.toString());
|
||||
|
||||
// Append ambient lighting shader
|
||||
await _player.command(['change-list', 'glsl-shaders', 'append', _shaderPath!]);
|
||||
|
||||
_enabled = true;
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('AmbientLightingService: Enabled (video=$videoAspect, output=$outputAspect)');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('AmbientLightingService: Failed to enable: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable ambient lighting effect and restore normal letterboxing.
|
||||
Future<void> disable() async {
|
||||
if (!_enabled) return;
|
||||
|
||||
try {
|
||||
if (_shaderPath != null) {
|
||||
await _player.command(['change-list', 'glsl-shaders', 'remove', _shaderPath!]);
|
||||
}
|
||||
|
||||
await _player.setProperty('video-aspect-override', 'no');
|
||||
|
||||
_enabled = false;
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('AmbientLightingService: Disabled');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('AmbientLightingService: Failed to disable: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-append the ambient lighting shader to the chain.
|
||||
/// Called by ShaderService after it rebuilds the shader chain (clr + append).
|
||||
Future<void> reappendShader() async {
|
||||
if (!_enabled || _shaderPath == null) return;
|
||||
await _player.command(['change-list', 'glsl-shaders', 'append', _shaderPath!]);
|
||||
}
|
||||
|
||||
/// Update video-aspect-override when the window resizes.
|
||||
/// The shader adapts automatically via dynamic `target_size` uniform.
|
||||
void updateOutputAspect(double outputAspect) {
|
||||
if (!_enabled) return;
|
||||
_player.setProperty('video-aspect-override', outputAspect.toString());
|
||||
}
|
||||
|
||||
/// Generate a static multi-pass GLSL shader.
|
||||
///
|
||||
/// Uses MPV's built-in `input_size` (video dimensions) and `target_size`
|
||||
/// (output dimensions) uniforms which update automatically on resize.
|
||||
/// No baked-in aspect constants — fully adaptive.
|
||||
///
|
||||
/// Pipeline (all MAIN hooks):
|
||||
/// 1. Save original video as ORIGINAL
|
||||
/// 2. Downscale to 1/8 as SMALL (center 60% zoom)
|
||||
/// 3-5. Three Kawase blur passes at 1/8 -> BLUR8C
|
||||
/// 6. Downscale BLUR8C to 1/64 as TINY
|
||||
/// 7-8. Two more Kawase blur passes at 1/64 -> GLOW
|
||||
/// 9. Composite: video rect -> ORIGINAL, bars -> GLOW
|
||||
String _generateShader() {
|
||||
final br = _brightness.toStringAsFixed(2);
|
||||
final buf = StringBuffer();
|
||||
|
||||
// Pass 1: Save original video
|
||||
buf.writeln('//!HOOK MAIN');
|
||||
buf.writeln('//!BIND HOOKED');
|
||||
buf.writeln('//!SAVE ORIGINAL');
|
||||
buf.writeln('//!DESC Ambient Lighting Save');
|
||||
buf.writeln('vec4 hook() {');
|
||||
buf.writeln(' return HOOKED_tex(HOOKED_pos);');
|
||||
buf.writeln('}');
|
||||
buf.writeln();
|
||||
|
||||
// Pass 2: Downscale to 1/8 with center zoom.
|
||||
buf.writeln('//!HOOK MAIN');
|
||||
buf.writeln('//!BIND ORIGINAL');
|
||||
buf.writeln('//!SAVE SMALL');
|
||||
buf.writeln('//!WIDTH ORIGINAL.w 8 /');
|
||||
buf.writeln('//!HEIGHT ORIGINAL.h 8 /');
|
||||
buf.writeln('//!DESC Ambient Lighting Downscale');
|
||||
buf.writeln('vec4 hook() {');
|
||||
buf.writeln(' vec2 uv = ORIGINAL_pos * 0.6 + 0.2;');
|
||||
buf.writeln(' return ORIGINAL_tex(uv);');
|
||||
buf.writeln('}');
|
||||
buf.writeln();
|
||||
|
||||
// Pass 3-5: Three Kawase blur passes at 1/8 resolution.
|
||||
const blur8Steps = [
|
||||
('SMALL', 'BLUR8A', '2.0', 'Blur1'),
|
||||
('BLUR8A', 'BLUR8B', '6.0', 'Blur2'),
|
||||
('BLUR8B', 'BLUR8C', '12.0', 'Blur3'),
|
||||
];
|
||||
for (final (input, output, offset, desc) in blur8Steps) {
|
||||
buf.writeln('//!HOOK MAIN');
|
||||
buf.writeln('//!BIND $input');
|
||||
buf.writeln('//!SAVE $output');
|
||||
buf.writeln('//!WIDTH $input.w');
|
||||
buf.writeln('//!HEIGHT $input.h');
|
||||
buf.writeln('//!DESC Ambient Lighting $desc');
|
||||
buf.writeln('vec4 hook() {');
|
||||
buf.writeln(' vec2 ps = ${input}_pt;');
|
||||
buf.writeln(' vec4 s = ${input}_tex(${input}_pos + vec2( $offset, $offset) * ps)');
|
||||
buf.writeln(' + ${input}_tex(${input}_pos + vec2( $offset, -$offset) * ps)');
|
||||
buf.writeln(' + ${input}_tex(${input}_pos + vec2(-$offset, $offset) * ps)');
|
||||
buf.writeln(' + ${input}_tex(${input}_pos + vec2(-$offset, -$offset) * ps);');
|
||||
buf.writeln(' return s * 0.25;');
|
||||
buf.writeln('}');
|
||||
buf.writeln();
|
||||
}
|
||||
|
||||
// Pass 6: Downscale the already-blurred 1/8 texture to 1/64.
|
||||
buf.writeln('//!HOOK MAIN');
|
||||
buf.writeln('//!BIND BLUR8C');
|
||||
buf.writeln('//!SAVE TINY');
|
||||
buf.writeln('//!WIDTH BLUR8C.w 8 /');
|
||||
buf.writeln('//!HEIGHT BLUR8C.h 8 /');
|
||||
buf.writeln('//!DESC Ambient Lighting Downscale2');
|
||||
buf.writeln('vec4 hook() {');
|
||||
buf.writeln(' return BLUR8C_tex(BLUR8C_pos);');
|
||||
buf.writeln('}');
|
||||
buf.writeln();
|
||||
|
||||
// Pass 7-8: Two more Kawase blur passes at 1/64 for maximum diffusion.
|
||||
const blur64Steps = [
|
||||
('TINY', 'GLOW1', '3.0', 'Blur4'),
|
||||
('GLOW1', 'GLOW', '6.0', 'Blur5'),
|
||||
];
|
||||
for (final (input, output, offset, desc) in blur64Steps) {
|
||||
buf.writeln('//!HOOK MAIN');
|
||||
buf.writeln('//!BIND $input');
|
||||
buf.writeln('//!SAVE $output');
|
||||
buf.writeln('//!WIDTH $input.w');
|
||||
buf.writeln('//!HEIGHT $input.h');
|
||||
buf.writeln('//!DESC Ambient Lighting $desc');
|
||||
buf.writeln('vec4 hook() {');
|
||||
buf.writeln(' vec2 ps = ${input}_pt;');
|
||||
buf.writeln(' vec4 s = ${input}_tex(${input}_pos + vec2( $offset, $offset) * ps)');
|
||||
buf.writeln(' + ${input}_tex(${input}_pos + vec2( $offset, -$offset) * ps)');
|
||||
buf.writeln(' + ${input}_tex(${input}_pos + vec2(-$offset, $offset) * ps)');
|
||||
buf.writeln(' + ${input}_tex(${input}_pos + vec2(-$offset, -$offset) * ps);');
|
||||
buf.writeln(' return s * 0.25;');
|
||||
buf.writeln('}');
|
||||
buf.writeln();
|
||||
}
|
||||
|
||||
// Pass 9: Composite — no //!SAVE so this replaces MAIN.
|
||||
// Uses built-in input_size (video coded dims) and target_size (output dims)
|
||||
// to dynamically compute where the video rect sits. These auto-update on resize.
|
||||
buf.writeln('//!HOOK MAIN');
|
||||
buf.writeln('//!BIND ORIGINAL');
|
||||
buf.writeln('//!BIND GLOW');
|
||||
buf.writeln('//!DESC Ambient Lighting Composite');
|
||||
buf.writeln('vec4 hook() {');
|
||||
buf.writeln(' float vid_aspect = input_size.x / input_size.y;');
|
||||
buf.writeln(' float out_aspect = target_size.x / target_size.y;');
|
||||
buf.writeln();
|
||||
buf.writeln(' vec2 scale, off;');
|
||||
buf.writeln(' if (vid_aspect > out_aspect) {');
|
||||
buf.writeln(' float s = out_aspect / vid_aspect;');
|
||||
buf.writeln(' scale = vec2(1.0, s);');
|
||||
buf.writeln(' off = vec2(0.0, (1.0 - s) * 0.5);');
|
||||
buf.writeln(' } else {');
|
||||
buf.writeln(' float s = vid_aspect / out_aspect;');
|
||||
buf.writeln(' scale = vec2(s, 1.0);');
|
||||
buf.writeln(' off = vec2((1.0 - s) * 0.5, 0.0);');
|
||||
buf.writeln(' }');
|
||||
buf.writeln();
|
||||
buf.writeln(' vec2 vid_pos = (ORIGINAL_pos - off) / scale;');
|
||||
buf.writeln();
|
||||
buf.writeln(' if (all(greaterThanEqual(vid_pos, vec2(0.0))) &&');
|
||||
buf.writeln(' all(lessThanEqual(vid_pos, vec2(1.0)))) {');
|
||||
buf.writeln(' return ORIGINAL_tex(vid_pos);');
|
||||
buf.writeln(' }');
|
||||
buf.writeln();
|
||||
buf.writeln(' return GLOW_tex(ORIGINAL_pos) * $br;');
|
||||
buf.writeln('}');
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/// Write the shader to a temp file and return the path.
|
||||
Future<String> _writeShaderToTemp(String shader) async {
|
||||
final cacheDir = await getTemporaryDirectory();
|
||||
final shaderDir = Directory(path.join(cacheDir.path, 'shaders'));
|
||||
if (!shaderDir.existsSync()) {
|
||||
shaderDir.createSync(recursive: true);
|
||||
}
|
||||
final file = File(path.join(shaderDir.path, 'ambient_lighting.glsl'));
|
||||
await file.writeAsString(shader);
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,21 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/shader_preset.dart';
|
||||
import '../mpv/player/player.dart';
|
||||
import 'ambient_lighting_service.dart';
|
||||
import 'shader_asset_loader.dart';
|
||||
|
||||
/// Service for applying GLSL shaders to the MPV video player.
|
||||
///
|
||||
/// Handles shader chain building, HDR detection, and runtime switching.
|
||||
/// When ambient lighting is active, the ambient lighting shader is always appended last
|
||||
/// in the chain after any upscaling/processing shaders.
|
||||
class ShaderService {
|
||||
final Player _player;
|
||||
ShaderPreset _currentPreset = ShaderPreset.none;
|
||||
|
||||
/// Reference to ambient lighting service for re-appending its shader after chain rebuilds.
|
||||
AmbientLightingService? ambientLightingService;
|
||||
|
||||
ShaderService(this._player);
|
||||
|
||||
/// The currently applied shader preset
|
||||
@@ -41,6 +47,7 @@ class ShaderService {
|
||||
}
|
||||
await _clearShaders();
|
||||
_currentPreset = ShaderPreset.none;
|
||||
await _reappendAmbientLighting();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +59,7 @@ class ShaderService {
|
||||
// No shaders - clear any existing ones
|
||||
await _clearShaders();
|
||||
_currentPreset = preset;
|
||||
await _reappendAmbientLighting();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,6 +73,9 @@ class ShaderService {
|
||||
|
||||
_currentPreset = preset;
|
||||
|
||||
// Re-append ambient lighting shader at end of chain
|
||||
await _reappendAmbientLighting();
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('ShaderService: Applied ${preset.name} with ${shaderPaths.length} shaders');
|
||||
}
|
||||
@@ -87,6 +98,21 @@ class ShaderService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-append the ambient lighting shader if it's active.
|
||||
/// Called after shader chain rebuilds to keep ambient lighting last.
|
||||
Future<void> _reappendAmbientLighting() async {
|
||||
final service = ambientLightingService;
|
||||
if (service == null || !service.isEnabled) return;
|
||||
|
||||
try {
|
||||
await service.reappendShader();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('ShaderService: Failed to re-append ambient lighting: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the current video content is HDR.
|
||||
Future<bool> _isHdrContent() async {
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../mpv/mpv.dart';
|
||||
|
||||
import '../models/plex_media_version.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'ambient_lighting_service.dart';
|
||||
|
||||
/// Manages video filtering, aspect ratio modes, and subtitle positioning for video playback.
|
||||
///
|
||||
@@ -13,6 +14,7 @@ import '../utils/app_logger.dart';
|
||||
/// - Video cropping calculations for fill screen mode
|
||||
/// - Subtitle positioning adjustments based on crop parameters
|
||||
/// - Debounced video filter updates on resize events
|
||||
/// - Ambient-lighting-friendly reset to contain mode
|
||||
class VideoFilterManager {
|
||||
final Player player;
|
||||
final List<PlexMediaVersion> availableVersions;
|
||||
@@ -24,6 +26,9 @@ class VideoFilterManager {
|
||||
/// Store the boxFitMode before entering PiP so it can be restored
|
||||
int? _prePipBoxFitMode;
|
||||
|
||||
/// Ambient lighting service reference - when active, video-aspect-override is managed by ambient lighting
|
||||
AmbientLightingService? ambientLightingService;
|
||||
|
||||
/// Track if a pinch gesture is occurring (public for gesture tracking)
|
||||
bool isPinching = false;
|
||||
|
||||
@@ -54,6 +59,14 @@ class VideoFilterManager {
|
||||
updateVideoFilter();
|
||||
}
|
||||
|
||||
/// Reset to contain mode (mode 0). Used when enabling ambient lighting.
|
||||
void resetToContain() {
|
||||
if (_boxFitMode != 0) {
|
||||
_boxFitMode = 0;
|
||||
updateVideoFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle between contain and cover modes only (for pinch gesture)
|
||||
void toggleContainCover() {
|
||||
_boxFitMode = _boxFitMode == 0 ? 1 : 0;
|
||||
@@ -89,11 +102,13 @@ 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.
|
||||
void updateVideoFilter() async {
|
||||
try {
|
||||
// Clear all video filters and manual scaling first
|
||||
await player.setProperty('video-aspect-override', 'no');
|
||||
if (ambientLightingService?.isEnabled != true) {
|
||||
await player.setProperty('video-aspect-override', 'no');
|
||||
}
|
||||
await player.setProperty('sub-ass-force-margins', 'no');
|
||||
await player.setProperty('panscan', '0');
|
||||
|
||||
|
||||
@@ -89,6 +89,12 @@ class DesktopVideoControls extends StatefulWidget {
|
||||
/// Channel name for live TV display
|
||||
final String? liveChannelName;
|
||||
|
||||
/// Whether ambient lighting is enabled (passed to settings sheet)
|
||||
final bool isAmbientLightingEnabled;
|
||||
|
||||
/// Called to toggle ambient lighting (passed to settings sheet)
|
||||
final VoidCallback? onToggleAmbientLighting;
|
||||
|
||||
const DesktopVideoControls({
|
||||
super.key,
|
||||
required this.player,
|
||||
@@ -135,6 +141,8 @@ class DesktopVideoControls extends StatefulWidget {
|
||||
this.thumbnailUrlBuilder,
|
||||
this.isLive = false,
|
||||
this.liveChannelName,
|
||||
this.isAmbientLightingEnabled = false,
|
||||
this.onToggleAmbientLighting,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -680,6 +688,8 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
isLive: widget.isLive,
|
||||
shaderService: widget.shaderService,
|
||||
onShaderChanged: widget.onShaderChanged,
|
||||
isAmbientLightingEnabled: widget.isAmbientLightingEnabled,
|
||||
onToggleAmbientLighting: widget.onToggleAmbientLighting,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -86,6 +86,12 @@ class VideoSettingsSheet extends StatefulWidget {
|
||||
/// Called when shader preset changes
|
||||
final VoidCallback? onShaderChanged;
|
||||
|
||||
/// Whether ambient lighting is currently enabled
|
||||
final bool isAmbientLightingEnabled;
|
||||
|
||||
/// Called to toggle ambient lighting on/off (null if unsupported)
|
||||
final VoidCallback? onToggleAmbientLighting;
|
||||
|
||||
const VideoSettingsSheet({
|
||||
super.key,
|
||||
required this.player,
|
||||
@@ -95,6 +101,8 @@ class VideoSettingsSheet extends StatefulWidget {
|
||||
this.isLive = false,
|
||||
this.shaderService,
|
||||
this.onShaderChanged,
|
||||
this.isAmbientLightingEnabled = false,
|
||||
this.onToggleAmbientLighting,
|
||||
});
|
||||
|
||||
static Future<void> show(
|
||||
@@ -108,6 +116,8 @@ class VideoSettingsSheet extends StatefulWidget {
|
||||
bool isLive = false,
|
||||
ShaderService? shaderService,
|
||||
VoidCallback? onShaderChanged,
|
||||
bool isAmbientLightingEnabled = false,
|
||||
VoidCallback? onToggleAmbientLighting,
|
||||
}) {
|
||||
return BaseVideoControlSheet.showSheet(
|
||||
context: context,
|
||||
@@ -121,6 +131,8 @@ class VideoSettingsSheet extends StatefulWidget {
|
||||
isLive: isLive,
|
||||
shaderService: shaderService,
|
||||
onShaderChanged: onShaderChanged,
|
||||
isAmbientLightingEnabled: isAmbientLightingEnabled,
|
||||
onToggleAmbientLighting: onToggleAmbientLighting,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -369,6 +381,29 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
onTap: () => _navigateTo(_SettingsView.shader),
|
||||
),
|
||||
|
||||
// Ambient Lighting (MPV only)
|
||||
if (widget.onToggleAmbientLighting != null)
|
||||
ListTile(
|
||||
leading: AppIcon(
|
||||
Symbols.blur_on,
|
||||
fill: 1,
|
||||
color: widget.isAmbientLightingEnabled ? Colors.amber : Colors.white70,
|
||||
),
|
||||
title: Text(t.videoControls.ambientLighting, style: const TextStyle(color: Colors.white)),
|
||||
trailing: Switch(
|
||||
value: widget.isAmbientLightingEnabled,
|
||||
onChanged: (_) {
|
||||
widget.onToggleAmbientLighting?.call();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
activeThumbColor: Colors.amber,
|
||||
),
|
||||
onTap: () {
|
||||
widget.onToggleAmbientLighting?.call();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
|
||||
// Performance Overlay Toggle
|
||||
ListTile(
|
||||
leading: AppIcon(
|
||||
|
||||
@@ -77,6 +77,8 @@ Widget plexVideoControlsBuilder(
|
||||
String Function(Duration time)? thumbnailUrlBuilder,
|
||||
bool isLive = false,
|
||||
String? liveChannelName,
|
||||
bool isAmbientLightingEnabled = false,
|
||||
VoidCallback? onToggleAmbientLighting,
|
||||
}) {
|
||||
return PlexVideoControls(
|
||||
player: player,
|
||||
@@ -101,6 +103,8 @@ Widget plexVideoControlsBuilder(
|
||||
thumbnailUrlBuilder: thumbnailUrlBuilder,
|
||||
isLive: isLive,
|
||||
liveChannelName: liveChannelName,
|
||||
isAmbientLightingEnabled: isAmbientLightingEnabled,
|
||||
onToggleAmbientLighting: onToggleAmbientLighting,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,6 +154,12 @@ class PlexVideoControls extends StatefulWidget {
|
||||
/// Channel name for live TV display
|
||||
final String? liveChannelName;
|
||||
|
||||
/// Whether ambient lighting is enabled (passed to settings sheet)
|
||||
final bool isAmbientLightingEnabled;
|
||||
|
||||
/// Called to toggle ambient lighting (passed to settings sheet)
|
||||
final VoidCallback? onToggleAmbientLighting;
|
||||
|
||||
const PlexVideoControls({
|
||||
super.key,
|
||||
required this.player,
|
||||
@@ -174,6 +184,8 @@ class PlexVideoControls extends StatefulWidget {
|
||||
this.thumbnailUrlBuilder,
|
||||
this.isLive = false,
|
||||
this.liveChannelName,
|
||||
this.isAmbientLightingEnabled = false,
|
||||
this.onToggleAmbientLighting,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -905,6 +917,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
isLive: widget.isLive,
|
||||
shaderService: widget.shaderService,
|
||||
onShaderChanged: widget.onShaderChanged,
|
||||
isAmbientLightingEnabled: widget.isAmbientLightingEnabled,
|
||||
onToggleAmbientLighting: widget.player.playerType != 'exoplayer' ? widget.onToggleAmbientLighting : null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1895,6 +1909,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
thumbnailUrlBuilder: widget.thumbnailUrlBuilder,
|
||||
isLive: widget.isLive,
|
||||
liveChannelName: widget.liveChannelName,
|
||||
isAmbientLightingEnabled: widget.isAmbientLightingEnabled,
|
||||
onToggleAmbientLighting: widget.player.playerType != 'exoplayer' ? widget.onToggleAmbientLighting : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ class TrackChapterControls extends StatelessWidget {
|
||||
final ShaderService? shaderService;
|
||||
final VoidCallback? onShaderChanged;
|
||||
|
||||
/// Whether ambient lighting is enabled (passed to settings sheet)
|
||||
final bool isAmbientLightingEnabled;
|
||||
|
||||
/// Called to toggle ambient lighting (passed to settings sheet)
|
||||
final VoidCallback? onToggleAmbientLighting;
|
||||
|
||||
/// List of FocusNodes for the buttons (passed from parent for navigation)
|
||||
final List<FocusNode>? focusNodes;
|
||||
|
||||
@@ -95,6 +101,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
this.isLive = false,
|
||||
this.shaderService,
|
||||
this.onShaderChanged,
|
||||
this.isAmbientLightingEnabled = false,
|
||||
this.onToggleAmbientLighting,
|
||||
});
|
||||
|
||||
/// Handle key event for button navigation
|
||||
@@ -201,6 +209,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
isLive: isLive,
|
||||
shaderService: shaderService,
|
||||
onShaderChanged: onShaderChanged,
|
||||
isAmbientLightingEnabled: isAmbientLightingEnabled,
|
||||
onToggleAmbientLighting: onToggleAmbientLighting,
|
||||
);
|
||||
onLoadSeekTimes?.call();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user