diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index d954783d..a9081e22 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -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", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 00dec1a0..476ee77f 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -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", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 2aa43f7f..d4ef4d88 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -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", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index f6f64dfa..3e3c64a0 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -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", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 9e780caf..2766d941 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -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", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index c2fef135..07db600b 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -320,6 +320,9 @@ "versionsButton": "동영상 버전", "pipButton": "픽처 인 픽처 모드", "aspectRatioButton": "화면비율", + "ambientLighting": "주변 조명", + "ambientLightingOn": "주변 조명 켜기", + "ambientLightingOff": "주변 조명 끄기", "fullscreenButton": "전체화면", "exitFullscreenButton": "전체화면 종료", "alwaysOnTopButton": "창 최상위 고정", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 96b77727..efdfba97 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -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", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 2b0a2e07..cac362cc 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -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 diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 804fd01e..73c33643 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -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', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index a3230baa..569886a1 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -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', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 781bbfaa..14b2b6f3 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -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', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 5ecd065b..4f19c0dc 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -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', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 8691243b..02f5c43c 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -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', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 4e0728bd..58e7e7ef 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -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' => '컬렉션', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index de11dcc3..0cb8c348 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -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', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index f83c15e5..845f4f5e 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -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', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 32fe7d62..5a90f090 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -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' => '管理', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index d4a719e0..61366681 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -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", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 41b9b450..be56349e 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -320,6 +320,9 @@ "versionsButton": "视频版本", "pipButton": "画中画模式", "aspectRatioButton": "宽高比", + "ambientLighting": "氛围灯光", + "ambientLightingOn": "开启氛围灯光", + "ambientLightingOff": "关闭氛围灯光", "fullscreenButton": "进入全屏", "exitFullscreenButton": "退出全屏", "alwaysOnTopButton": "置顶窗口", diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 22850fc0..28e6a01f 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -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 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 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 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 _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 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 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 with WidgetsBindin : null, isLive: widget.isLive, liveChannelName: _liveChannelName, + isAmbientLightingEnabled: _ambientLightingService?.isEnabled ?? false, + onToggleAmbientLighting: _toggleAmbientLighting, ), ); }, diff --git a/lib/services/ambient_lighting_service.dart b/lib/services/ambient_lighting_service.dart new file mode 100644 index 00000000..7fbf76a1 --- /dev/null +++ b/lib/services/ambient_lighting_service.dart @@ -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 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 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 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 _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; + } +} diff --git a/lib/services/shader_service.dart b/lib/services/shader_service.dart index b386c7bd..b5f64286 100644 --- a/lib/services/shader_service.dart +++ b/lib/services/shader_service.dart @@ -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 _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 _isHdrContent() async { try { diff --git a/lib/services/video_filter_manager.dart b/lib/services/video_filter_manager.dart index 424694e5..7eb43aa2 100644 --- a/lib/services/video_filter_manager.dart +++ b/lib/services/video_filter_manager.dart @@ -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 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'); diff --git a/lib/widgets/video_controls/desktop_video_controls.dart b/lib/widgets/video_controls/desktop_video_controls.dart index 2c9b44df..20307a35 100644 --- a/lib/widgets/video_controls/desktop_video_controls.dart +++ b/lib/widgets/video_controls/desktop_video_controls.dart @@ -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 { isLive: widget.isLive, shaderService: widget.shaderService, onShaderChanged: widget.onShaderChanged, + isAmbientLightingEnabled: widget.isAmbientLightingEnabled, + onToggleAmbientLighting: widget.onToggleAmbientLighting, ), ], ), diff --git a/lib/widgets/video_controls/sheets/video_settings_sheet.dart b/lib/widgets/video_controls/sheets/video_settings_sheet.dart index cd6dd658..05b35b19 100644 --- a/lib/widgets/video_controls/sheets/video_settings_sheet.dart +++ b/lib/widgets/video_controls/sheets/video_settings_sheet.dart @@ -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 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 { 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( diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index 2aca1036..86088773 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -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 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 with WindowListen thumbnailUrlBuilder: widget.thumbnailUrlBuilder, isLive: widget.isLive, liveChannelName: widget.liveChannelName, + isAmbientLightingEnabled: widget.isAmbientLightingEnabled, + onToggleAmbientLighting: widget.player.playerType != 'exoplayer' ? widget.onToggleAmbientLighting : null, ), ); } diff --git a/lib/widgets/video_controls/widgets/track_chapter_controls.dart b/lib/widgets/video_controls/widgets/track_chapter_controls.dart index 3e4054b6..b13cd417 100644 --- a/lib/widgets/video_controls/widgets/track_chapter_controls.dart +++ b/lib/widgets/video_controls/widgets/track_chapter_controls.dart @@ -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? 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(); },