From 44773fd742ed9dfb02bcb0035ff4b0bf8418fc3d Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 31 Aug 2026 06:43:47 +0200 Subject: [PATCH] feat(player): remember the swipe brightness level between playbacks The brightness set with the left-edge swipe was lost the moment playback ended, so every session started back at the system level. A new "Remember Brightness Level" toggle under Settings > Playback > Gestures persists the level a swipe settles on and reapplies it when the next playback starts (and when the app resumes mid-session). Leaving the player still restores the pre-playback brightness, so the rest of the app is unaffected. Default off, preserving current behavior. close #2178 --- lib/i18n/az.i18n.json | 2 + lib/i18n/bg.i18n.json | 2 + lib/i18n/da.i18n.json | 2 + lib/i18n/de.i18n.json | 2 + lib/i18n/en.i18n.json | 2 + lib/i18n/es.i18n.json | 2 + lib/i18n/fr.i18n.json | 2 + lib/i18n/hu.i18n.json | 2 + lib/i18n/it.i18n.json | 2 + lib/i18n/ja.i18n.json | 2 + lib/i18n/kk.i18n.json | 2 + lib/i18n/ko.i18n.json | 2 + lib/i18n/nb.i18n.json | 2 + lib/i18n/nl.i18n.json | 2 + lib/i18n/pl.i18n.json | 2 + lib/i18n/pt.i18n.json | 2 + lib/i18n/ru.i18n.json | 2 + lib/i18n/strings.g.dart | 2 +- lib/i18n/strings_en.g.dart | 20 +- lib/i18n/sv.i18n.json | 2 + lib/i18n/tr.i18n.json | 2 + lib/i18n/uz.i18n.json | 2 + lib/i18n/zh-Hant.i18n.json | 2 + lib/i18n/zh.i18n.json | 2 + .../settings/playback_settings_screen.dart | 7 + lib/services/device_adjustment_service.dart | 12 + lib/services/settings_service.dart | 12 + .../video_controls/parts/playback_input.dart | 33 ++ .../video_controls/video_controls.dart | 9 +- ...deo_controls_remember_brightness_test.dart | 284 ++++++++++++++++++ 30 files changed, 412 insertions(+), 11 deletions(-) create mode 100644 test/widgets/video_controls_remember_brightness_test.dart diff --git a/lib/i18n/az.i18n.json b/lib/i18n/az.i18n.json index 8b04fcd1b..68488d591 100644 --- a/lib/i18n/az.i18n.json +++ b/lib/i18n/az.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Səsi tənzimləmək üçün sağ kənarda yuxarı və ya aşağı sürüşdürün", "gesturePinchToZoom": "Çimdiklə yaxınlaşdır", "gesturePinchToZoomDescription": "Yaxınlaşdırmaq və ya uzaqlaşdırmaq üçün videoda çimdik hərəkəti edin", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "İdarəetmələr", "rememberPlayerChanges": "Pleyer dəyişikliklərini yadda saxla", "rememberPlayerChangesDescription": "Oxutma zamanı edilən dəyişikliklərin harada saxlanacağı və yenidən tətbiq ediləcəyi", diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 82654a283..bfb9080e6 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Плъзни нагоре или надолу по десния ръб, за да регулираш силата на звука", "gesturePinchToZoom": "Стискане за мащабиране", "gesturePinchToZoomDescription": "Стисни видеото, за да увеличиш или намалиш мащаба", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Контроли", "rememberPlayerChanges": "Запомняне на промените в плейъра", "rememberPlayerChangesDescription": "Къде се записва и откъде се прилага отново промяна, направена по време на възпроизвеждане", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 51812e874..11c489597 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Stryg op eller ned i højre kant for at justere lydstyrken", "gesturePinchToZoom": "Klem for at zoome", "gesturePinchToZoomDescription": "Klem på videoen for at zoome ind eller ud", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Kontroller", "rememberPlayerChanges": "Husk afspillerændringer", "rememberPlayerChangesDescription": "Hvor en ændring under afspilning gemmes og anvendes igen", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index f343f21dc..33d63eea6 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Wische am rechten Rand nach oben oder unten, um die Lautstärke anzupassen", "gesturePinchToZoom": "Zum Zoomen kneifen", "gesturePinchToZoomDescription": "Kneife auf dem Video, um hinein- oder herauszuzoomen", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Steuerung", "rememberPlayerChanges": "Playeränderungen merken", "rememberPlayerChangesDescription": "Wo eine während der Wiedergabe vorgenommene Änderung gespeichert und erneut angewendet wird", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 7f16b49f0..49e44f2fe 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Swipe up or down on the right edge to adjust volume", "gesturePinchToZoom": "Pinch to Zoom", "gesturePinchToZoomDescription": "Pinch on the video to zoom in or out", + "rememberBrightnessLevel": "Remember Brightness Level", + "rememberBrightnessLevelDescription": "Start playback at the brightness set by the last swipe", "controls": "Controls", "rememberPlayerChanges": "Remember Player Changes", "rememberPlayerChangesDescription": "Where a change made during playback is saved and reapplied from", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index b42c959cb..38cee003a 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Desliza hacia arriba o abajo en el borde derecho para ajustar el volumen", "gesturePinchToZoom": "Pellizcar para hacer zoom", "gesturePinchToZoomDescription": "Pellizca el video para acercar o alejar", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Controles", "rememberPlayerChanges": "Recordar cambios del reproductor", "rememberPlayerChangesDescription": "Dónde se guarda y se vuelve a aplicar un cambio realizado durante la reproducción", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index d72dbb84a..8bdf68481 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Balayez vers le haut ou le bas sur le bord droit pour régler le volume", "gesturePinchToZoom": "Pincer pour zoomer", "gesturePinchToZoomDescription": "Pincez la vidéo pour zoomer ou dézoomer", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Commandes", "rememberPlayerChanges": "Mémoriser les modifications du lecteur", "rememberPlayerChangesDescription": "Où une modification effectuée pendant la lecture est enregistrée et réappliquée", diff --git a/lib/i18n/hu.i18n.json b/lib/i18n/hu.i18n.json index d5f88e7bb..149daff17 100644 --- a/lib/i18n/hu.i18n.json +++ b/lib/i18n/hu.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Húzd felfelé vagy lefelé a jobb szélén a hangerő beállításához", "gesturePinchToZoom": "Csípés a nagyításhoz", "gesturePinchToZoomDescription": "Csípj a videóra a nagyításhoz vagy kicsinyítéshez", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Vezérlők", "rememberPlayerChanges": "Lejátszó módosításainak megjegyzése", "rememberPlayerChangesDescription": "A lejátszás közben végzett módosítások mentési és újbóli alkalmazási helye", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 431cf590e..3887db920 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Scorri verso l'alto o il basso sul bordo destro per regolare il volume", "gesturePinchToZoom": "Pizzica per lo zoom", "gesturePinchToZoomDescription": "Pizzica il video per ingrandire o ridurre", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Controlli", "rememberPlayerChanges": "Ricorda le modifiche del lettore", "rememberPlayerChangesDescription": "Dove viene salvata e riapplicata una modifica effettuata durante la riproduzione", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index d1ee773a7..783913f9a 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "右端を上下にスワイプして音量を調整します", "gesturePinchToZoom": "ピンチでズーム", "gesturePinchToZoomDescription": "動画をピンチしてズームイン・アウト", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "コントロール", "rememberPlayerChanges": "プレーヤーの変更を記憶", "rememberPlayerChangesDescription": "再生中に行った変更を保存し、再適用する場所", diff --git a/lib/i18n/kk.i18n.json b/lib/i18n/kk.i18n.json index 2a7edd48a..b90430010 100644 --- a/lib/i18n/kk.i18n.json +++ b/lib/i18n/kk.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Оң жақ шетінде жоғары немесе төмен сырғытып, дыбыс деңгейін реттеу", "gesturePinchToZoom": "Шымшу арқылы масштабтау", "gesturePinchToZoomDescription": "Видеода шымшып, жақындату немесе алыстату", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Басқару элементтері", "rememberPlayerChanges": "Ойнатқыш өзгерістерін есте сақтау", "rememberPlayerChangesDescription": "Ойнату кезінде жасалған өзгеріс сақталатын және қайта қолданылатын орын", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index a593b66b4..fb29c5aae 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "오른쪽 가장자리에서 위아래로 스와이프하여 볼륨을 조절합니다", "gesturePinchToZoom": "핀치 줌", "gesturePinchToZoomDescription": "비디오에서 핀치하여 확대하거나 축소합니다", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "컨트롤", "rememberPlayerChanges": "플레이어 변경 사항 기억", "rememberPlayerChangesDescription": "재생 중 변경한 사항을 저장하고 다시 적용할 위치", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index 4b815326f..c0613c170 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Sveip opp eller ned på høyre kant for å justere volumet", "gesturePinchToZoom": "Klyp for å zoome", "gesturePinchToZoomDescription": "Klyp på videoen for å zoome inn eller ut", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Kontroller", "rememberPlayerChanges": "Husk endringer i spilleren", "rememberPlayerChangesDescription": "Hvor en endring under avspilling lagres og brukes på nytt", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 5c698db45..c4ffd2b34 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Veeg op de rechterrand omhoog of omlaag om het volume aan te passen", "gesturePinchToZoom": "Knijpen om te zoomen", "gesturePinchToZoomDescription": "Knijp op de video om in of uit te zoomen", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Bediening", "rememberPlayerChanges": "Spelerwijzigingen onthouden", "rememberPlayerChangesDescription": "Waar een wijziging tijdens het afspelen wordt opgeslagen en opnieuw toegepast", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 8d174011e..88f72d5aa 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Przesuwaj palcem w górę lub w dół na prawej krawędzi, aby regulować głośność", "gesturePinchToZoom": "Powiększanie ściskaniem", "gesturePinchToZoomDescription": "Ściśnij wideo palcami, aby powiększyć lub pomniejszyć", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Sterowanie", "rememberPlayerChanges": "Zapamiętuj zmiany odtwarzacza", "rememberPlayerChangesDescription": "Miejsce zapisywania i ponownego stosowania zmian dokonanych podczas odtwarzania", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index fe242a954..4d7d243d7 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Deslize para cima ou para baixo na borda direita para ajustar o volume", "gesturePinchToZoom": "Pinça para zoom", "gesturePinchToZoomDescription": "Pince o vídeo para ampliar ou reduzir", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Controles", "rememberPlayerChanges": "Lembrar alterações do reprodutor", "rememberPlayerChangesDescription": "Onde uma alteração feita durante a reprodução é salva e reaplicada", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index ddee0ca4e..d75d13954 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Проведите вверх или вниз по правому краю, чтобы изменить громкость", "gesturePinchToZoom": "Щипок для масштабирования", "gesturePinchToZoomDescription": "Сведите или разведите пальцы на видео, чтобы изменить масштаб", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Элементы управления", "rememberPlayerChanges": "Запоминать изменения плеера", "rememberPlayerChangesDescription": "Где сохраняется и откуда повторно применяется изменение, сделанное во время воспроизведения", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index caa104d0f..58220153b 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 22 -/// Strings: 43736 (1988 per locale) +/// Strings: 43738 (1988 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 4757caff5..dfa4dd41e 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -1203,6 +1203,12 @@ class Translations$settings$en { /// en: 'Pinch on the video to zoom in or out' String get gesturePinchToZoomDescription => 'Pinch on the video to zoom in or out'; + /// en: 'Remember Brightness Level' + String get rememberBrightnessLevel => 'Remember Brightness Level'; + + /// en: 'Start playback at the brightness set by the last swipe' + String get rememberBrightnessLevelDescription => 'Start playback at the brightness set by the last swipe'; + /// en: 'Controls' String get controls => 'Controls'; @@ -7288,6 +7294,8 @@ extension on Translations { 'settings.gestureVolumeSwipeDescription' => 'Swipe up or down on the right edge to adjust volume', 'settings.gesturePinchToZoom' => 'Pinch to Zoom', 'settings.gesturePinchToZoomDescription' => 'Pinch on the video to zoom in or out', + 'settings.rememberBrightnessLevel' => 'Remember Brightness Level', + 'settings.rememberBrightnessLevelDescription' => 'Start playback at the brightness set by the last swipe', 'settings.controls' => 'Controls', 'settings.rememberPlayerChanges' => 'Remember Player Changes', 'settings.rememberPlayerChangesDescription' => 'Where a change made during playback is saved and reapplied from', @@ -7424,10 +7432,10 @@ extension on Translations { 'fileInfo.streamPath' => 'Stream Path', 'fileInfo.optimizedForStreaming' => 'Optimized for Streaming', 'fileInfo.has64bitOffsets' => '64-bit Offsets', - 'fileInfo.protocol' => 'Protocol', - 'fileInfo.mediaType' => 'Media Type', _ => null, } ?? switch (path) { + 'fileInfo.protocol' => 'Protocol', + 'fileInfo.mediaType' => 'Media Type', 'fileInfo.sourceKind' => 'Source Kind', 'fileInfo.optimizedVersion' => 'Optimized Version', 'fileInfo.optimizationTarget' => 'Optimization Target', @@ -7938,10 +7946,10 @@ extension on Translations { 'libraries.filterCategories.genre' => 'Genre', 'libraries.filterCategories.year' => 'Year', 'libraries.filterCategories.contentRating' => 'Content Rating', - 'libraries.filterCategories.tag' => 'Tag', - 'libraries.filterCategories.unwatched' => 'Unwatched', _ => null, } ?? switch (path) { + 'libraries.filterCategories.tag' => 'Tag', + 'libraries.filterCategories.unwatched' => 'Unwatched', 'libraries.filterCategories.unplayed' => 'Unplayed', 'libraries.filterCategories.favorites' => 'Favorites', 'libraries.sortLabels.title' => 'Title', @@ -8452,10 +8460,10 @@ extension on Translations { 'downloads.syncRuleAndDownloadsRemoved' => 'Sync rule and associated downloads removed', 'downloads.syncRuleCleanupBusy' => 'Sync rules are currently updating. Try again in a moment.', 'downloads.syncRuleCleanupUnavailable' => 'Associated downloads could not be identified safely. Reconnect the server and try again, or remove the rule without deleting downloads.', - 'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => 'Synced ${count} new episodes for ${title}', - 'downloads.activeSyncRules' => 'Sync rules', _ => null, } ?? switch (path) { + 'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => 'Synced ${count} new episodes for ${title}', + 'downloads.activeSyncRules' => 'Sync rules', 'downloads.noSyncRules' => 'No sync rules', 'downloads.manageSyncRule' => 'Manage sync', 'downloads.editEpisodeCount' => 'Episode count', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 948d54d2a..4866b11f2 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Svep uppåt eller nedåt i högerkanten för att justera volymen", "gesturePinchToZoom": "Nyp för att zooma", "gesturePinchToZoomDescription": "Nyp på videon för att zooma in eller ut", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Kontroller", "rememberPlayerChanges": "Kom ihåg spelarändringar", "rememberPlayerChangesDescription": "Var en ändring under uppspelning sparas och tillämpas igen", diff --git a/lib/i18n/tr.i18n.json b/lib/i18n/tr.i18n.json index d7007a500..26ca69a4f 100644 --- a/lib/i18n/tr.i18n.json +++ b/lib/i18n/tr.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Sesi ayarlamak için sağ kenarda yukarı veya aşağı kaydırın", "gesturePinchToZoom": "Kıstırarak Yakınlaştır", "gesturePinchToZoomDescription": "Yakınlaştırmak veya uzaklaştırmak için videoyu kıstırın", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Kontroller", "rememberPlayerChanges": "Oynatıcı değişikliklerini hatırla", "rememberPlayerChangesDescription": "Oynatma sırasında yapılan değişikliklerin kaydedilip yeniden uygulanacağı yer", diff --git a/lib/i18n/uz.i18n.json b/lib/i18n/uz.i18n.json index fb3af27bf..af1a4bdce 100644 --- a/lib/i18n/uz.i18n.json +++ b/lib/i18n/uz.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "Ovozni sozlash uchun oʻng chekkada yuqoriga yoki pastga suring", "gesturePinchToZoom": "Chimchilab masshtablash", "gesturePinchToZoomDescription": "Videoni yaqinlashtirish yoki uzoqlashtirish uchun chimchilang", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "Boshqaruv elementlari", "rememberPlayerChanges": "Pleyer oʻzgarishlarini eslab qolish", "rememberPlayerChangesDescription": "Ijro vaqtida qilingan oʻzgarish qayerda saqlanishi va qayta qoʻllanishi", diff --git a/lib/i18n/zh-Hant.i18n.json b/lib/i18n/zh-Hant.i18n.json index 4564bcc2a..6c3072d20 100644 --- a/lib/i18n/zh-Hant.i18n.json +++ b/lib/i18n/zh-Hant.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "在螢幕右側邊緣上下滑動以調整音量", "gesturePinchToZoom": "雙指縮放", "gesturePinchToZoomDescription": "在影片上雙指捏合以放大或縮小", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "控制", "rememberPlayerChanges": "記住播放器變更", "rememberPlayerChangesDescription": "播放期間所做的變更要儲存並從何處重新套用", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 2580a0a25..3eb78e6af 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -388,6 +388,8 @@ "gestureVolumeSwipeDescription": "在屏幕右边缘上下滑动以调节音量", "gesturePinchToZoom": "双指缩放", "gesturePinchToZoomDescription": "在视频上双指捏合以放大或缩小", + "rememberBrightnessLevel": "", + "rememberBrightnessLevelDescription": "", "controls": "控制", "rememberPlayerChanges": "记住播放器更改", "rememberPlayerChangesDescription": "播放期间所做的更改保存并重新应用的位置", diff --git a/lib/screens/settings/playback_settings_screen.dart b/lib/screens/settings/playback_settings_screen.dart index 1b09c887b..f7b102c15 100644 --- a/lib/screens/settings/playback_settings_screen.dart +++ b/lib/screens/settings/playback_settings_screen.dart @@ -383,6 +383,13 @@ class _PlaybackSettingsScreenState extends State { title: t.settings.gestureBrightnessSwipe, subtitle: t.settings.gestureBrightnessSwipeDescription, ), + // Remember the last swiped level between playbacks (#2178). + SettingSwitchTile( + pref: SettingsService.rememberBrightnessLevel, + icon: Symbols.settings_brightness_rounded, + title: t.settings.rememberBrightnessLevel, + subtitle: t.settings.rememberBrightnessLevelDescription, + ), SettingSwitchTile( pref: SettingsService.gestureVolumeSwipe, icon: Symbols.volume_up_rounded, diff --git a/lib/services/device_adjustment_service.dart b/lib/services/device_adjustment_service.dart index cf3a5a59c..bca812430 100644 --- a/lib/services/device_adjustment_service.dart +++ b/lib/services/device_adjustment_service.dart @@ -118,6 +118,18 @@ class DeviceAdjustmentService { onResume?.call(); } + /// Reset queue and restore state between tests. A queue future chained in + /// one `testWidgets` fake-async zone never completes inside the next test's + /// zone, which would stall every later brightness operation on the shared + /// [instance]. + @visibleForTesting + void resetForTesting() { + dispose(); + _brightnessChanged = false; + _brightnessGeneration = 0; + _brightnessQueue = Future.value(); + } + void dispose() { _lifecycleListener?.dispose(); _lifecycleListener = null; diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 78643c920..db9b72757 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -556,6 +556,16 @@ class SettingsService extends BaseSharedPreferencesService { static const gestureVolumeSwipe = BoolPref('gesture_volume_swipe', defaultValue: true); static const gesturePinchToZoom = BoolPref('gesture_pinch_to_zoom', defaultValue: true); + /// Remember the brightness level set by the swipe gesture (#2178). When on, + /// playback starts at [rememberedBrightnessLevel] instead of the system + /// level; the player exit still restores the pre-playback brightness. + static const rememberBrightnessLevel = BoolPref('remember_brightness_level'); + + /// Last brightness the swipe gesture settled on while + /// [rememberBrightnessLevel] was enabled. Negative means "never set"; + /// device-local runtime state, so reset-only in the registry. + static const rememberedBrightnessLevel = DoublePref('remembered_brightness_level', defaultValue: -1.0); + /// Deinterlace interlaced video via mpv's `deinterlace=auto` (#2149). /// mpv-only by design: ExoPlayer has no filter chain. static const deinterlace = BoolPref('deinterlace'); @@ -1128,6 +1138,7 @@ class SettingsService extends BaseSharedPreferencesService { gestureBrightnessSwipe, gestureVolumeSwipe, gesturePinchToZoom, + rememberBrightnessLevel, deinterlace, playerAlwaysOnTop, specialsOrdering, @@ -1230,6 +1241,7 @@ class SettingsService extends BaseSharedPreferencesService { customExternalPlayers, customRelayUrl, companionRemoteLastHostAddress, + rememberedBrightnessLevel, ]; /// Settings that "Reset All Settings" actually resets. diff --git a/lib/widgets/video_controls/parts/playback_input.dart b/lib/widgets/video_controls/parts/playback_input.dart index d433a8ee6..02604ff12 100644 --- a/lib/widgets/video_controls/parts/playback_input.dart +++ b/lib/widgets/video_controls/parts/playback_input.dart @@ -373,6 +373,34 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState { unawaited(_readEdgeAdjustmentValue(MobileEdgeAdjustmentSide.right)); } + /// Player entry and resume both funnel here: reapply the remembered swipe + /// brightness (#2178) before re-reading the gesture baselines, so the next + /// swipe starts from the level actually on screen. + void _handleDeviceAdjustmentResume() => unawaited(_applyRememberedBrightnessThenRefresh()); + + Future _applyRememberedBrightnessThenRefresh() async { + final settings = SettingsService.instance; + if (settings.read(SettingsService.rememberBrightnessLevel) && + settings.read(SettingsService.gestureBrightnessSwipe)) { + final value = settings.read(SettingsService.rememberedBrightnessLevel); + // Negative means "never set"; the write below only stores 0.0-1.0. + if (value >= 0.0 && value <= 1.0) { + // Await the queued set so the baseline read cannot race past it. + await _deviceAdjustmentService.setBrightness(value); + } + } + if (mounted) _refreshDeviceAdjustmentValues(); + } + + /// Persist the level a finished brightness swipe settled on (#2178). Runs + /// once per gesture, not per write, to spare SharedPreferences the drag spam. + void _persistRememberedBrightness() { + if (!SettingsService.instance.read(SettingsService.rememberBrightnessLevel)) return; + final value = _lastKnownBrightness; + if (value == null) return; + unawaited(SettingsService.instance.write(SettingsService.rememberedBrightnessLevel, value)); + } + Future _readEdgeAdjustmentValue(MobileEdgeAdjustmentSide side) { ++_edgeAdjustmentBaselineGeneration; _edgeAdjustmentBaselineSide = side; @@ -496,6 +524,7 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState { _edgeAdjustmentIndicatorHideTimer?.cancel(); _edgeAdjustmentIndicatorClearTimer?.cancel(); _edgeAdjustmentWasActive = true; + _edgeAdjustmentActiveSide = side; _edgeAdjustmentStartValue = startValue; _lastEdgeAdjustmentWriteAt = null; _lastEdgeAdjustmentWriteValue = null; @@ -542,7 +571,11 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState { void _finishEdgeAdjustment({required bool suppressTap}) { if (suppressTap) _suppressTouchTaps(); + if (_edgeAdjustmentWasActive && _edgeAdjustmentActiveSide == MobileEdgeAdjustmentSide.left) { + _persistRememberedBrightness(); + } _edgeAdjustmentWasActive = false; + _edgeAdjustmentActiveSide = null; _edgeAdjustmentStartValue = null; _lastEdgeAdjustmentWriteAt = null; _lastEdgeAdjustmentWriteValue = null; diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index bfa7a846c..4d2f7452b 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -818,6 +818,7 @@ class _PlexVideoControlsState extends State )); double? _edgeAdjustmentStartValue; bool _edgeAdjustmentWasActive = false; + MobileEdgeAdjustmentSide? _edgeAdjustmentActiveSide; MobileEdgeAdjustmentSide? _pendingEdgeAdjustmentSide; double _pendingEdgeAdjustmentDelta = 0.0; int? _pendingEdgeAdjustmentGeneration; @@ -960,12 +961,12 @@ class _PlexVideoControlsState extends State _listenToPlayingState(); _listenToCompleted(); _checkPipSupport(); - _deviceAdjustmentService.onResume = _refreshDeviceAdjustmentValues; + _deviceAdjustmentService.onResume = _handleDeviceAdjustmentResume; _deviceAdjustmentService.setRestoreSuppressed(_pipService.isPipActive.value); _pipService.isPipActive.addListener(_onEdgeAdjustmentPipChanged); _edgeAdjustmentLifecycleListener = AppLifecycleListener( - onResume: _refreshDeviceAdjustmentValues, - onShow: _refreshDeviceAdjustmentValues, + onResume: _handleDeviceAdjustmentResume, + onShow: _handleDeviceAdjustmentResume, onHide: _cancelEdgeAdjustmentGesture, onPause: _cancelEdgeAdjustmentGesture, ); @@ -1001,7 +1002,7 @@ class _PlexVideoControlsState extends State // shortcut. if (!_focusPlayPauseIfKeyboardMode()) _claimPlayerSurfaceFocus(); if (PlatformDetector.isMobile(context) && !PlatformDetector.isTV()) { - _refreshDeviceAdjustmentValues(); + _handleDeviceAdjustmentResume(); } }); } diff --git a/test/widgets/video_controls_remember_brightness_test.dart b/test/widgets/video_controls_remember_brightness_test.dart new file mode 100644 index 000000000..2b32e8d57 --- /dev/null +++ b/test/widgets/video_controls_remember_brightness_test.dart @@ -0,0 +1,284 @@ +import 'package:drift/native.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:provider/provider.dart'; + +import 'package:plezy/database/app_database.dart'; +import 'package:plezy/i18n/strings.g.dart'; +import 'package:plezy/mpv/mpv.dart'; +import 'package:plezy/providers/playback_state_provider.dart'; +import 'package:plezy/services/device_adjustment_service.dart'; +import 'package:plezy/services/settings_service.dart'; +import 'package:plezy/services/video_volume_controller.dart'; +import 'package:plezy/utils/platform_detector.dart'; +import 'package:plezy/watch_together/providers/watch_together_provider.dart'; +import 'package:plezy/widgets/video_controls/player_chrome_controller.dart'; +import 'package:plezy/widgets/video_controls/video_controls.dart'; +import 'package:plezy/widgets/video_controls/widgets/player_toast_indicator.dart'; + +import '../test_helpers/media_items.dart'; +import '../test_helpers/prefs.dart'; +import '../test_helpers/theme.dart'; + +/// Remember Brightness Level (#2178): with the gestures-menu toggle on, the +/// level a brightness swipe settled on is persisted and reapplied when the +/// next playback starts; the player exit still restores the pre-playback +/// brightness so the rest of the app is untouched. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const adjustmentChannel = MethodChannel('com.plezy/device_adjustment'); + + late _RecordingPlayer player; + late PlayerChromeController chrome; + late PlayerToastController toast; + late VideoVolumeController volume; + late PlaybackStateProvider playbackState; + late WatchTogetherProvider watchTogether; + late AppDatabase database; + late SettingsService settings; + late List adjustmentCalls; + late double nativeBrightness; + + setUp(() async { + LocaleSettings.setLocaleSync(AppLocale.en); + await initializeDateFormatting('en'); + resetSharedPreferencesForTest(); + SettingsService.resetForTesting(); + settings = await SettingsService.getInstance(); + + // Phone layout: the touch pointer pipeline and the post-frame device + // adjustment hook are wired only when isMobile(context) && !isTV(). + TvDetectionService.debugSetAppleTVOverride(false); + PlatformDetector.debugSetIsDesktopOSOverride(false); + + adjustmentCalls = []; + nativeBrightness = 0.5; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(adjustmentChannel, ( + call, + ) async { + adjustmentCalls.add(call); + switch (call.method) { + case 'getBrightness': + return nativeBrightness; + case 'setBrightness': + nativeBrightness = call.arguments as double; + return null; + case 'getMediaVolume': + return 0.5; + default: + return null; + } + }); + + database = AppDatabase.forTesting(NativeDatabase.memory()); + player = _RecordingPlayer(); + chrome = PlayerChromeController(); + toast = PlayerToastController(); + volume = VideoVolumeController(player: player, settings: settings, initialVolume: 100); + playbackState = PlaybackStateProvider(); + watchTogether = WatchTogetherProvider(); + }); + + tearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(adjustmentChannel, null); + TvDetectionService.debugSetAppleTVOverride(null); + PlatformDetector.debugSetIsDesktopOSOverride(null); + volume.dispose(); + playbackState.dispose(); + watchTogether.dispose(); + chrome.dispose(); + toast.dispose(); + await database.close(); + }); + + const surface = Size(800, 600); + + Iterable callsTo(String method) => adjustmentCalls.where((c) => c.method == method); + + Future pumpControls(WidgetTester tester) async { + // Reset the shared brightness queue *inside* this test's fake-async zone: + // a queue future minted in setUp (real zone) or a previous test's zone + // schedules its completion on a microtask queue this test never flushes. + DeviceAdjustmentService.instance.resetForTesting(); + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: database), + ChangeNotifierProvider.value(value: playbackState), + ChangeNotifierProvider.value(value: watchTogether), + ], + child: MaterialApp( + theme: ThemeData(platform: TargetPlatform.android, extensions: const [testMonoTokens]), + home: Scaffold( + body: SizedBox( + width: surface.width, + height: surface.height, + child: PlexVideoControls( + player: player, + volumeController: volume, + metadata: testMediaItem(id: 'remember-brightness'), + toastController: toast, + chromeController: chrome, + canNavigateMediaItems: false, + canControl: true, + ), + ), + ), + ), + ), + ); + // Post-frame device-adjustment hook, then the queued channel round trips. + await tester.pump(); + await tester.pump(); + // Swipes start from hidden chrome — visible controls cover the left edge + // zone and swallow the pointer before the edge Listener sees it. + chrome.hide(); + chrome.markControlsHidden(); + await tester.pump(); + expect(chrome.controlsVisible, isFalse); + } + + Future settle(WidgetTester tester) async { + chrome.cancelAutoHide(); + toast.hide(); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + } + + /// One vertical edge swipe: down, resolve the baseline read, drag, lift. + /// 84px up over the 420px active band (600 minus the 15% exclusion bands) + /// is a +0.2 brightness/volume delta. + Future edgeSwipe(WidgetTester tester, {required double x}) async { + final origin = tester.getRect(find.byType(PlexVideoControls)).topLeft; + final gesture = await tester.startGesture(origin + Offset(x, 400)); + await tester.pump(); // Resolve the candidate baseline read deterministically. + await gesture.moveTo(origin + Offset(x, 316)); + await tester.pump(); + await gesture.up(); + await tester.pump(); + } + + testWidgets('playback opens at the remembered level and exit still restores', (tester) async { + await settings.write(SettingsService.rememberBrightnessLevel, true); + await settings.write(SettingsService.rememberedBrightnessLevel, 0.35); + + await pumpControls(tester); + + expect(callsTo('setBrightness').map((c) => c.arguments), [closeTo(0.35, 0.001)]); + + await settle(tester); + expect( + callsTo('restoreBrightness'), + hasLength(1), + reason: 'leaving the player must not darken the rest of the app', + ); + }); + + testWidgets('an unset remembered level writes nothing at startup', (tester) async { + await settings.write(SettingsService.rememberBrightnessLevel, true); + + await pumpControls(tester); + + expect(callsTo('setBrightness'), isEmpty); + + await settle(tester); + }); + + testWidgets('with the toggle off a stored level is left alone at startup', (tester) async { + await settings.write(SettingsService.rememberedBrightnessLevel, 0.35); + + await pumpControls(tester); + + expect(callsTo('setBrightness'), isEmpty); + + await settle(tester); + }); + + testWidgets('a disabled brightness swipe never reapplies a remembered level', (tester) async { + await settings.write(SettingsService.rememberBrightnessLevel, true); + await settings.write(SettingsService.rememberedBrightnessLevel, 0.35); + await settings.write(SettingsService.gestureBrightnessSwipe, false); + + await pumpControls(tester); + + expect(callsTo('setBrightness'), isEmpty); + + await settle(tester); + }); + + testWidgets('a finished brightness swipe persists the level it settled on', (tester) async { + await settings.write(SettingsService.rememberBrightnessLevel, true); + + await pumpControls(tester); + await edgeSwipe(tester, x: 40); + + expect(callsTo('setBrightness').map((c) => c.arguments).last, closeTo(0.7, 0.001)); + expect(settings.read(SettingsService.rememberedBrightnessLevel), closeTo(0.7, 0.001)); + + await settle(tester); + }); + + testWidgets('with the toggle off a brightness swipe persists nothing', (tester) async { + await pumpControls(tester); + await edgeSwipe(tester, x: 40); + + expect(callsTo('setBrightness'), isNotEmpty, reason: 'the gesture itself stays live without the toggle'); + expect(settings.read(SettingsService.rememberedBrightnessLevel), -1.0); + + await settle(tester); + }); + + testWidgets('a volume swipe never writes the remembered brightness', (tester) async { + await settings.write(SettingsService.rememberBrightnessLevel, true); + + await pumpControls(tester); + await edgeSwipe(tester, x: surface.width - 40); + + expect(callsTo('setMediaVolume'), isNotEmpty); + expect(settings.read(SettingsService.rememberedBrightnessLevel), -1.0); + + await settle(tester); + }); +} + +/// Minimal [Player] with a fixed playing state against a 45-minute item. +class _RecordingPlayer implements Player { + @override + String get playerType => 'mpv'; + + @override + PlayerState get state => PlayerState( + playing: true, + position: const Duration(minutes: 10), + duration: const Duration(minutes: 45), + seekable: true, + ); + + @override + PlayerStreams get streams => PlayerStreams( + playing: const Stream.empty(), + completed: const Stream.empty(), + buffering: const Stream.empty(), + position: const Stream.empty(), + duration: const Stream.empty(), + seekable: const Stream.empty(), + buffer: const Stream.empty(), + volume: const Stream.empty(), + rate: const Stream.empty(), + tracks: const Stream.empty(), + track: const Stream.empty(), + log: const Stream.empty(), + error: const Stream.empty(), + audioDevice: const Stream.empty(), + audioDevices: const Stream>.empty(), + bufferRanges: const Stream>.empty(), + playbackRestart: const Stream.empty(), + backendSwitched: const Stream.empty(), + ); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +}