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>
75 lines
1.6 KiB
Dart
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;
|
|
}
|
|
}
|