mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 15:03:01 +02:00
270 lines
7.3 KiB
Dart
270 lines
7.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../models/pomodoro_state.dart';
|
|
import 'notification_service.dart';
|
|
import 'sound_service.dart';
|
|
import 'sync_service.dart';
|
|
|
|
/// Manages the Pomodoro timer logic, independent of UI framework.
|
|
///
|
|
/// Optionally synchronizes state across devices via [SyncService].
|
|
class PomodoroTimer extends ChangeNotifier {
|
|
/// Creates a [PomodoroTimer] with configurable durations.
|
|
PomodoroTimer({
|
|
int? workMinutes,
|
|
int? shortBreakMinutes,
|
|
int? longBreakMinutes,
|
|
int? pomodorosPerCycle,
|
|
TimerStyle timerStyle = TimerStyle.pomodoro,
|
|
this.syncService,
|
|
SoundService? soundService,
|
|
NotificationService? notificationService,
|
|
@visibleForTesting Timer Function(Duration, void Function(Timer))? timerFactory,
|
|
}) : _timerStyle = timerStyle,
|
|
_soundService = soundService,
|
|
_notificationService = notificationService,
|
|
_timerFactory = timerFactory ?? Timer.periodic {
|
|
_workMinutes = workMinutes ?? timerStyle.defaultWorkMinutes;
|
|
_shortBreakMinutes = shortBreakMinutes ?? timerStyle.defaultShortBreakMinutes;
|
|
_longBreakMinutes = longBreakMinutes ?? timerStyle.defaultLongBreakMinutes;
|
|
_pomodorosPerCycle = pomodorosPerCycle ?? timerStyle.defaultPomodorosPerCycle;
|
|
_state = PomodoroState.initial(
|
|
workMinutes: _workMinutes,
|
|
shortBreakMinutes: _shortBreakMinutes,
|
|
longBreakMinutes: _longBreakMinutes,
|
|
pomodorosPerCycle: _pomodorosPerCycle,
|
|
);
|
|
}
|
|
|
|
/// Duration of a work session in minutes.
|
|
late int _workMinutes;
|
|
|
|
/// Duration of a short break in minutes.
|
|
late int _shortBreakMinutes;
|
|
|
|
/// Duration of a long break in minutes.
|
|
late int _longBreakMinutes;
|
|
|
|
/// Number of work sessions before a long break.
|
|
late int _pomodorosPerCycle;
|
|
|
|
/// The current timer style.
|
|
TimerStyle _timerStyle;
|
|
|
|
/// Optional sync service for LAN synchronization.
|
|
final SyncService? syncService;
|
|
|
|
final SoundService? _soundService;
|
|
final NotificationService? _notificationService;
|
|
final Timer Function(Duration, void Function(Timer)) _timerFactory;
|
|
|
|
late PomodoroState _state;
|
|
Timer? _timer;
|
|
|
|
/// Whether we are currently applying a remote state (prevents echo).
|
|
bool _applyingRemote = false;
|
|
|
|
/// The current state of the timer.
|
|
PomodoroState get state => _state;
|
|
|
|
/// The active timer style.
|
|
TimerStyle get timerStyle => _timerStyle;
|
|
|
|
/// Switches to a different timer style, resetting all progress.
|
|
void switchStyle(TimerStyle style) {
|
|
if (style == _timerStyle) return;
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
_timerStyle = style;
|
|
_workMinutes = style.defaultWorkMinutes;
|
|
_shortBreakMinutes = style.defaultShortBreakMinutes;
|
|
_longBreakMinutes = style.defaultLongBreakMinutes;
|
|
_pomodorosPerCycle = style.defaultPomodorosPerCycle;
|
|
_state = PomodoroState.initial(
|
|
workMinutes: _workMinutes,
|
|
shortBreakMinutes: _shortBreakMinutes,
|
|
longBreakMinutes: _longBreakMinutes,
|
|
pomodorosPerCycle: _pomodorosPerCycle,
|
|
);
|
|
_notificationService?.cancel();
|
|
notifyListeners();
|
|
syncService?.stopHeartbeat();
|
|
}
|
|
|
|
/// Starts or resumes the timer.
|
|
void start() {
|
|
if (_state.isRunning) return;
|
|
_state = _state.copyWith(isRunning: true);
|
|
notifyListeners();
|
|
_startTicking();
|
|
_notificationService?.showTimer(state: _state);
|
|
_broadcastIfLocal('start');
|
|
syncService?.startHeartbeat(() => _state);
|
|
}
|
|
|
|
/// Pauses the timer.
|
|
void pause() {
|
|
if (!_state.isRunning) return;
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
_state = _state.copyWith(isRunning: false);
|
|
_notificationService?.cancel();
|
|
notifyListeners();
|
|
_broadcastIfLocal('pause');
|
|
syncService?.stopHeartbeat();
|
|
}
|
|
|
|
/// Resets the current session timer without changing the mode.
|
|
void reset() {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
_state = _state.copyWith(
|
|
remainingSeconds: _state.totalSeconds,
|
|
isRunning: false,
|
|
);
|
|
_notificationService?.cancel();
|
|
notifyListeners();
|
|
_broadcastIfLocal('reset');
|
|
syncService?.stopHeartbeat();
|
|
}
|
|
|
|
/// Skips to the next session, treating the current one as completed.
|
|
void skip() {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
_onSessionComplete();
|
|
_broadcastIfLocal('skip');
|
|
syncService?.stopHeartbeat();
|
|
}
|
|
|
|
/// Applies state received from a remote device via [SyncService].
|
|
void applyRemoteState(PomodoroState remoteState, String action) {
|
|
_applyingRemote = true;
|
|
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
|
|
_state = remoteState;
|
|
|
|
if (_state.isRunning) {
|
|
_startTicking();
|
|
}
|
|
|
|
notifyListeners();
|
|
_applyingRemote = false;
|
|
}
|
|
|
|
void _tick(Timer timer) {
|
|
if (_state.remainingSeconds <= 1) {
|
|
timer.cancel();
|
|
_timer = null;
|
|
_onSessionComplete();
|
|
} else {
|
|
_state = _state.copyWith(
|
|
remainingSeconds: _state.remainingSeconds - 1,
|
|
);
|
|
_updateNotification();
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void _onSessionComplete() {
|
|
if (_state.mode == PomodoroMode.work) {
|
|
final newCompleted = _state.completedPomodoros + 1;
|
|
_state = _state.copyWith(
|
|
completedPomodoros: newCompleted,
|
|
remainingSeconds: 0,
|
|
isRunning: false,
|
|
);
|
|
} else {
|
|
_state = _state.copyWith(
|
|
remainingSeconds: 0,
|
|
isRunning: false,
|
|
);
|
|
}
|
|
notifyListeners();
|
|
final completedMode = _state.mode;
|
|
_advanceToNextMode();
|
|
_soundService?.playTransitionSound(
|
|
completedMode: completedMode,
|
|
nextMode: _state.mode,
|
|
);
|
|
_notificationService?.showSessionComplete(
|
|
completedMode: completedMode,
|
|
nextMode: _state.mode,
|
|
);
|
|
notifyListeners();
|
|
}
|
|
|
|
void _advanceToNextMode() {
|
|
switch (_state.mode) {
|
|
case PomodoroMode.work:
|
|
if (_state.completedPomodoros > 0 &&
|
|
_state.completedPomodoros % _pomodorosPerCycle == 0) {
|
|
_setMode(PomodoroMode.longBreak);
|
|
} else {
|
|
_setMode(PomodoroMode.shortBreak);
|
|
}
|
|
case PomodoroMode.shortBreak:
|
|
case PomodoroMode.longBreak:
|
|
_setMode(PomodoroMode.work);
|
|
}
|
|
}
|
|
|
|
void _setMode(PomodoroMode mode) {
|
|
final totalSeconds = _durationForMode(mode) * 60;
|
|
_state = _state.copyWith(
|
|
mode: mode,
|
|
remainingSeconds: totalSeconds,
|
|
totalSeconds: totalSeconds,
|
|
isRunning: false,
|
|
);
|
|
}
|
|
|
|
int _durationForMode(PomodoroMode mode) {
|
|
switch (mode) {
|
|
case PomodoroMode.work:
|
|
return _workMinutes;
|
|
case PomodoroMode.shortBreak:
|
|
return _shortBreakMinutes;
|
|
case PomodoroMode.longBreak:
|
|
return _longBreakMinutes;
|
|
}
|
|
}
|
|
|
|
void _startTicking() {
|
|
_timer = _timerFactory(
|
|
const Duration(seconds: 1),
|
|
_tick,
|
|
);
|
|
}
|
|
|
|
/// Broadcasts state to peers only if this is a local user action.
|
|
void _broadcastIfLocal(String action) {
|
|
if (!_applyingRemote) {
|
|
syncService?.broadcast(_state, action);
|
|
}
|
|
}
|
|
|
|
/// Interval in seconds between notification updates while running.
|
|
static const _notifyIntervalSeconds = 30;
|
|
|
|
void _updateNotification() {
|
|
if (_notificationService == null) return;
|
|
if (_state.remainingSeconds % _notifyIntervalSeconds == 0) {
|
|
_notificationService.showTimer(state: _state);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
syncService?.stopHeartbeat();
|
|
_soundService?.dispose();
|
|
_notificationService?.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|