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
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -388,6 +388,8 @@
|
||||
"gestureVolumeSwipeDescription": "Плъзни нагоре или надолу по десния ръб, за да регулираш силата на звука",
|
||||
"gesturePinchToZoom": "Стискане за мащабиране",
|
||||
"gesturePinchToZoomDescription": "Стисни видеото, за да увеличиш или намалиш мащаба",
|
||||
"rememberBrightnessLevel": "",
|
||||
"rememberBrightnessLevelDescription": "",
|
||||
"controls": "Контроли",
|
||||
"rememberPlayerChanges": "Запомняне на промените в плейъра",
|
||||
"rememberPlayerChangesDescription": "Къде се записва и откъде се прилага отново промяна, направена по време на възпроизвеждане",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -388,6 +388,8 @@
|
||||
"gestureVolumeSwipeDescription": "右端を上下にスワイプして音量を調整します",
|
||||
"gesturePinchToZoom": "ピンチでズーム",
|
||||
"gesturePinchToZoomDescription": "動画をピンチしてズームイン・アウト",
|
||||
"rememberBrightnessLevel": "",
|
||||
"rememberBrightnessLevelDescription": "",
|
||||
"controls": "コントロール",
|
||||
"rememberPlayerChanges": "プレーヤーの変更を記憶",
|
||||
"rememberPlayerChangesDescription": "再生中に行った変更を保存し、再適用する場所",
|
||||
|
||||
@@ -388,6 +388,8 @@
|
||||
"gestureVolumeSwipeDescription": "Оң жақ шетінде жоғары немесе төмен сырғытып, дыбыс деңгейін реттеу",
|
||||
"gesturePinchToZoom": "Шымшу арқылы масштабтау",
|
||||
"gesturePinchToZoomDescription": "Видеода шымшып, жақындату немесе алыстату",
|
||||
"rememberBrightnessLevel": "",
|
||||
"rememberBrightnessLevelDescription": "",
|
||||
"controls": "Басқару элементтері",
|
||||
"rememberPlayerChanges": "Ойнатқыш өзгерістерін есте сақтау",
|
||||
"rememberPlayerChangesDescription": "Ойнату кезінде жасалған өзгеріс сақталатын және қайта қолданылатын орын",
|
||||
|
||||
@@ -388,6 +388,8 @@
|
||||
"gestureVolumeSwipeDescription": "오른쪽 가장자리에서 위아래로 스와이프하여 볼륨을 조절합니다",
|
||||
"gesturePinchToZoom": "핀치 줌",
|
||||
"gesturePinchToZoomDescription": "비디오에서 핀치하여 확대하거나 축소합니다",
|
||||
"rememberBrightnessLevel": "",
|
||||
"rememberBrightnessLevelDescription": "",
|
||||
"controls": "컨트롤",
|
||||
"rememberPlayerChanges": "플레이어 변경 사항 기억",
|
||||
"rememberPlayerChangesDescription": "재생 중 변경한 사항을 저장하고 다시 적용할 위치",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -388,6 +388,8 @@
|
||||
"gestureVolumeSwipeDescription": "Проведите вверх или вниз по правому краю, чтобы изменить громкость",
|
||||
"gesturePinchToZoom": "Щипок для масштабирования",
|
||||
"gesturePinchToZoomDescription": "Сведите или разведите пальцы на видео, чтобы изменить масштаб",
|
||||
"rememberBrightnessLevel": "",
|
||||
"rememberBrightnessLevelDescription": "",
|
||||
"controls": "Элементы управления",
|
||||
"rememberPlayerChanges": "Запоминать изменения плеера",
|
||||
"rememberPlayerChangesDescription": "Где сохраняется и откуда повторно применяется изменение, сделанное во время воспроизведения",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -388,6 +388,8 @@
|
||||
"gestureVolumeSwipeDescription": "在螢幕右側邊緣上下滑動以調整音量",
|
||||
"gesturePinchToZoom": "雙指縮放",
|
||||
"gesturePinchToZoomDescription": "在影片上雙指捏合以放大或縮小",
|
||||
"rememberBrightnessLevel": "",
|
||||
"rememberBrightnessLevelDescription": "",
|
||||
"controls": "控制",
|
||||
"rememberPlayerChanges": "記住播放器變更",
|
||||
"rememberPlayerChangesDescription": "播放期間所做的變更要儲存並從何處重新套用",
|
||||
|
||||
@@ -388,6 +388,8 @@
|
||||
"gestureVolumeSwipeDescription": "在屏幕右边缘上下滑动以调节音量",
|
||||
"gesturePinchToZoom": "双指缩放",
|
||||
"gesturePinchToZoomDescription": "在视频上双指捏合以放大或缩小",
|
||||
"rememberBrightnessLevel": "",
|
||||
"rememberBrightnessLevelDescription": "",
|
||||
"controls": "控制",
|
||||
"rememberPlayerChanges": "记住播放器更改",
|
||||
"rememberPlayerChangesDescription": "播放期间所做的更改保存并重新应用的位置",
|
||||
|
||||
@@ -383,6 +383,13 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||
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,
|
||||
|
||||
@@ -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<void>.value();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_lifecycleListener?.dispose();
|
||||
_lifecycleListener = null;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<void> _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<double?> _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;
|
||||
|
||||
@@ -818,6 +818,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
));
|
||||
double? _edgeAdjustmentStartValue;
|
||||
bool _edgeAdjustmentWasActive = false;
|
||||
MobileEdgeAdjustmentSide? _edgeAdjustmentActiveSide;
|
||||
MobileEdgeAdjustmentSide? _pendingEdgeAdjustmentSide;
|
||||
double _pendingEdgeAdjustmentDelta = 0.0;
|
||||
int? _pendingEdgeAdjustmentGeneration;
|
||||
@@ -960,12 +961,12 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
_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<PlexVideoControls>
|
||||
// shortcut.
|
||||
if (!_focusPlayPauseIfKeyboardMode()) _claimPlayerSurfaceFocus();
|
||||
if (PlatformDetector.isMobile(context) && !PlatformDetector.isTV()) {
|
||||
_refreshDeviceAdjustmentValues();
|
||||
_handleDeviceAdjustmentResume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<MethodCall> 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 = <MethodCall>[];
|
||||
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<MethodCall> callsTo(String method) => adjustmentCalls.where((c) => c.method == method);
|
||||
|
||||
Future<void> 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<AppDatabase>.value(value: database),
|
||||
ChangeNotifierProvider<PlaybackStateProvider>.value(value: playbackState),
|
||||
ChangeNotifierProvider<WatchTogetherProvider>.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<void> 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<void> 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<bool>.empty(),
|
||||
completed: const Stream<bool>.empty(),
|
||||
buffering: const Stream<bool>.empty(),
|
||||
position: const Stream<Duration>.empty(),
|
||||
duration: const Stream<Duration>.empty(),
|
||||
seekable: const Stream<bool>.empty(),
|
||||
buffer: const Stream<Duration>.empty(),
|
||||
volume: const Stream<double>.empty(),
|
||||
rate: const Stream<double>.empty(),
|
||||
tracks: const Stream<Tracks>.empty(),
|
||||
track: const Stream<TrackSelection>.empty(),
|
||||
log: const Stream<PlayerLog>.empty(),
|
||||
error: const Stream<PlayerError>.empty(),
|
||||
audioDevice: const Stream<AudioDevice>.empty(),
|
||||
audioDevices: const Stream<List<AudioDevice>>.empty(),
|
||||
bufferRanges: const Stream<List<BufferRange>>.empty(),
|
||||
playbackRestart: const Stream<void>.empty(),
|
||||
backendSwitched: const Stream<void>.empty(),
|
||||
);
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
Reference in New Issue
Block a user