fix(music): mark only the active sleep timer preset selected

All timed presets shared one 'timed' bool, so arming any of 15/30/60
minutes checkmarked all three. The service now retains the armed
duration (sleepTimerDuration) and the menu marks the matching preset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
edde746
2026-07-06 15:31:29 +02:00
co-authored by Claude Fable 5
parent 32c4810c21
commit 226be37f85
4 changed files with 33 additions and 1 deletions
+1 -1
View File
@@ -209,7 +209,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
value: '$minutes',
icon: Symbols.timer_rounded,
label: t.music.sleepTimerMinutes(n: minutes),
selected: timed,
selected: timed && service.sleepTimerDuration == Duration(minutes: minutes),
),
AppMenuItem(
value: 'end_of_track',
@@ -117,6 +117,10 @@ abstract class MusicPlaybackService extends ChangeNotifier {
/// inactive.
DateTime? get sleepTimerEndsAt;
/// The duration the timed sleep timer was armed with (for marking the
/// chosen preset); null in end-of-track mode or when inactive.
Duration? get sleepTimerDuration;
/// Whether the sleep timer pauses at the end of the current track instead
/// of after a fixed duration.
bool get sleepTimerEndOfTrack;
@@ -233,6 +237,9 @@ class StubMusicPlaybackService extends MusicPlaybackService {
@override
DateTime? get sleepTimerEndsAt => null;
@override
Duration? get sleepTimerDuration => null;
@override
bool get sleepTimerEndOfTrack => false;
@@ -129,6 +129,7 @@ class MusicPlaybackServiceImpl extends MusicPlaybackService with WidgetsBindingO
Timer? _sleepTimer;
DateTime? _sleepTimerEndsAt;
Duration? _sleepTimerDuration;
bool _sleepTimerEndOfTrack = false;
final StreamController<Duration> _positionController = StreamController<Duration>.broadcast();
@@ -186,6 +187,9 @@ class MusicPlaybackServiceImpl extends MusicPlaybackService with WidgetsBindingO
@override
DateTime? get sleepTimerEndsAt => _sleepTimerEndsAt;
@override
Duration? get sleepTimerDuration => _sleepTimerDuration;
@override
bool get sleepTimerEndOfTrack => _sleepTimerEndOfTrack;
@@ -932,10 +936,12 @@ class MusicPlaybackServiceImpl extends MusicPlaybackService with WidgetsBindingO
_sleepTimer?.cancel();
_sleepTimer = null;
_sleepTimerEndsAt = null;
_sleepTimerDuration = null;
final hadEndOfTrack = _sleepTimerEndOfTrack;
_sleepTimerEndOfTrack = endOfTrack;
if (!endOfTrack && duration != null) {
_sleepTimerEndsAt = DateTime.now().add(duration);
_sleepTimerDuration = duration;
_sleepTimer = Timer(duration, _onSleepTimerFired);
}
// End-of-track mode suppresses gapless arming (and leaving it restores
@@ -949,6 +955,7 @@ class MusicPlaybackServiceImpl extends MusicPlaybackService with WidgetsBindingO
void _onSleepTimerFired() {
_sleepTimer = null;
_sleepTimerEndsAt = null;
_sleepTimerDuration = null;
unawaited(pause());
notifyListeners();
}
@@ -957,6 +964,7 @@ class MusicPlaybackServiceImpl extends MusicPlaybackService with WidgetsBindingO
_sleepTimer?.cancel();
_sleepTimer = null;
_sleepTimerEndsAt = null;
_sleepTimerDuration = null;
_sleepTimerEndOfTrack = false;
}
@@ -929,6 +929,23 @@ void main() {
});
});
test('timed sleep timer exposes its armed duration for the preset menu', () async {
await h.playTracks([t1]);
h.service.setSleepTimer(const Duration(minutes: 30));
expect(h.service.sleepTimerActive, isTrue);
expect(h.service.sleepTimerDuration, const Duration(minutes: 30));
expect(h.service.sleepTimerEndOfTrack, isFalse);
h.service.setSleepTimer(null, endOfTrack: true);
expect(h.service.sleepTimerDuration, isNull);
expect(h.service.sleepTimerActive, isTrue);
h.service.setSleepTimer(null);
expect(h.service.sleepTimerActive, isFalse);
expect(h.service.sleepTimerDuration, isNull);
});
test('end-of-track sleep timer suppresses arming and pauses at completion', () async {
await h.playTracks([t1, t2]);
expect(h.player.armed?.uri, _urlFor(t2));