feat(player): add end-of-video sleep timer option (#1111)

close #540
This commit is contained in:
Timo Kox
2026-05-30 07:16:01 +02:00
committed by GitHub
parent a4f0b92b4e
commit 294996eac1
39 changed files with 604 additions and 143 deletions
+114
View File
@@ -287,6 +287,120 @@ void main() {
});
});
// ============================================================
// armEndOfVideo / notifyVideoCompleted
// ============================================================
group('armEndOfVideo', () {
test('sets isActive and isEndOfVideoMode without starting a periodic timer', () {
timer.armEndOfVideo(() {});
try {
expect(timer.isActive, isTrue);
expect(timer.isEndOfVideoMode, isTrue);
// No fixed duration / endTime in this mode — countdown UIs must
// detect end-of-video mode instead of falling through.
expect(timer.endTime, isNull);
expect(timer.duration, isNull);
expect(timer.originalDuration, isNull);
expect(timer.remainingTime, isNull);
} finally {
timer.cancelTimer();
}
});
test('replaces any running duration-based timer', () {
var firstFired = false;
timer.startTimer(const Duration(minutes: 30), () => firstFired = true);
expect(timer.isEndOfVideoMode, isFalse);
timer.armEndOfVideo(() {});
expect(timer.isEndOfVideoMode, isTrue);
// The previous duration is gone — armEndOfVideo calls cancelTimer first.
expect(timer.originalDuration, isNull);
expect(firstFired, isFalse);
timer.cancelTimer();
});
test('cancelTimer clears end-of-video mode', () {
timer.armEndOfVideo(() {});
timer.cancelTimer();
expect(timer.isActive, isFalse);
expect(timer.isEndOfVideoMode, isFalse);
});
test('arming notifies listeners', () {
var notifications = 0;
void listener() => notifications++;
timer.addListener(listener);
timer.armEndOfVideo(() {});
// cancelTimer (called inside armEndOfVideo when idle) is gated on existing
// state, so a fresh service only emits the single arm notification.
expect(notifications, greaterThanOrEqualTo(1));
timer.removeListener(listener);
timer.cancelTimer();
});
});
group('notifyVideoCompleted', () {
test('fires the stored callback and emits onCompleted', () async {
var fired = 0;
var completedFired = 0;
final sub = timer.onCompleted.listen((_) => completedFired++);
timer.armEndOfVideo(() => fired++);
timer.notifyVideoCompleted();
await Future<void>.delayed(Duration.zero);
expect(fired, 1);
expect(completedFired, 1);
// Mode is consumed after firing.
expect(timer.isEndOfVideoMode, isFalse);
expect(timer.isActive, isFalse);
await sub.cancel();
});
test('does nothing when end-of-video mode is not armed', () async {
var completedFired = 0;
final sub = timer.onCompleted.listen((_) => completedFired++);
timer.notifyVideoCompleted();
await Future<void>.delayed(Duration.zero);
expect(completedFired, 0);
// Also safe when a duration timer is running — should not interfere.
timer.startTimer(const Duration(minutes: 30), () {});
timer.notifyVideoCompleted();
await Future<void>.delayed(Duration.zero);
expect(completedFired, 0);
expect(timer.isActive, isTrue);
await sub.cancel();
timer.cancelTimer();
});
});
group('end-of-video + restartIfNeeded', () {
test('preserves the end-of-video mode across a playback session swap', () {
timer.armEndOfVideo(() {});
timer.markNeedsRestart();
var newCallbackHooked = false;
timer.restartIfNeeded(() => newCallbackHooked = true);
expect(timer.isActive, isTrue);
expect(timer.isEndOfVideoMode, isTrue);
// restartIfNeeded re-arms — the consumer callback should be wired up
// but not yet invoked.
expect(newCallbackHooked, isFalse);
timer.cancelTimer();
});
});
// ============================================================
// What's NOT covered (and why)
// ============================================================