Files
Melo/lib/player/sleep_timer.dart
Dustin-Mike Jens HähnelandClaude Opus 4.8 51c2229148 Add sleep timer feature
Timer stops playback (pause) after 5/10/15/30/60 min. Wall-clock
deadline is self-correcting against OS throttling; 1Hz tick drives
the countdown UI. Cancelled in stop() so no timer resurrects a paused
player after teardown.

- sleep_timer.dart: isolated time logic, injectable clock for testing
- audio_handler.dart: sleepTimer field + cancel() in stop()
- now_playing_screen.dart: AppBar button + bottom sheet
- 10 unit tests via mocked clock

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 20:06:08 +02:00

75 lines
1.6 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
class SleepTimer {
final VoidCallback onElapsed;
final DateTime Function() now;
late final ValueNotifier<Duration?> remaining = ValueNotifier(null);
Timer? _timer;
DateTime? _deadline;
bool _firedThisSession = false;
SleepTimer({
required this.onElapsed,
DateTime Function()? now,
}) : now = now ?? DateTime.now;
void start(Duration duration) {
if (duration <= Duration.zero) {
cancel();
return;
}
cancel(); // Replace any running timer
_firedThisSession = false;
// ponytail: Wall-Clock-Deadline, selbstkorrigierend gegen OS-Throttling.
// Nachteil: manuelle Systemuhr-Änderung feuert früh/spät — akzeptiert.
_deadline = now().add(duration);
_updateRemaining();
// Start periodic updates (1Hz for UI)
_timer = Timer.periodic(const Duration(seconds: 1), (_) => tick());
}
void tick() {
_updateRemaining();
_checkElapsed();
}
void _checkElapsed() {
if (_deadline == null || _firedThisSession) return;
if (!now().isBefore(_deadline!)) {
_firedThisSession = true;
_deadline = null;
_timer?.cancel();
_timer = null;
remaining.value = null;
onElapsed();
}
}
void _updateRemaining() {
if (_deadline == null) {
remaining.value = null;
return;
}
final diff = _deadline!.difference(now());
if (diff.isNegative) {
remaining.value = null;
} else {
remaining.value = diff;
}
}
void cancel() {
_timer?.cancel();
_timer = null;
_deadline = null;
_firedThisSession = false;
remaining.value = null;
}
}