Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class TaskDetailsContentState extends State<TaskDetailsContent> {

// Set to track which optional fields are visible
final Set<String> _visibleOptionalFields = {};
Duration _timeSinceLastSave = Duration.zero;

// Define optional field keys
static const String keyTags = 'tags';
Expand Down Expand Up @@ -1400,22 +1401,51 @@ class TaskDetailsContentState extends State<TaskDetailsContent> {
),
child: AppTimer(
isMiniLayout: true,
onTick: _handleTimerTick,
onTimerStop: _onTaskTimerStop,
onWorkSessionComplete: _onTaskWorkSessionComplete,
),
),
);

// Timer event handlers
void _onTaskTimerStop(Duration totalElapsed) {
void _handleTimerTick(Duration elapsedIncrement) {
// Use the elapsed increment provided by the timer
_timeSinceLastSave += elapsedIncrement;
if (_timeSinceLastSave.inSeconds >= TaskUiConstants.kPeriodicSaveIntervalSeconds) {
_saveTaskTime(_timeSinceLastSave);
_timeSinceLastSave = Duration.zero;
}
}

void _saveTaskTime(Duration elapsed) {
if (!mounted) return;
if (_task?.id == null) return;
if (elapsed.inSeconds <= 0) return;

// Only save if there's actual time elapsed
if (totalElapsed.inSeconds > 0) {
final command =
AddTaskTimeRecordCommand(duration: totalElapsed.inSeconds, taskId: _task!.id, customDateTime: DateTime.now());
_mediator.send(command);
_tasksService.notifyTaskUpdated(_task!.id);
final command = AddTaskTimeRecordCommand(
duration: elapsed.inSeconds,
taskId: _task!.id,
customDateTime: DateTime.now(),
);
_mediator.send(command);
_tasksService.notifyTaskUpdated(_task!.id);
}

/// Called when a work session completes (e.g., Pomodoro work → break transition).
/// Flushes any accumulated elapsed time for the current task.
void _onTaskWorkSessionComplete(Duration totalElapsed) {
if (_timeSinceLastSave > Duration.zero) {
_saveTaskTime(_timeSinceLastSave);
_timeSinceLastSave = Duration.zero;
}
}

/// Called when the timer actually stops (user stops / session ends).
/// Flushes accumulated elapsed time for the current task.
void _onTaskTimerStop(Duration totalElapsed) {
if (_timeSinceLastSave > Duration.zero) {
_saveTaskTime(_timeSinceLastSave);
_timeSinceLastSave = Duration.zero;
}
}
}
71 changes: 38 additions & 33 deletions src/lib/presentation/ui/features/tasks/components/timer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,21 @@ import 'package:whph/presentation/ui/shared/services/abstraction/i_translation_s
import 'package:whph/presentation/ui/shared/utils/app_theme_helper.dart';

class AppTimer extends StatefulWidget {
final Function(Duration)? onTick; // For UI updates only - receives current elapsed/remaining time
/// Callback invoked on each timer tick with the logical elapsed increment.
/// In debug mode, the increment is 1 minute per tick; in release, 1 second per tick.
/// Callers must use the elapsedIncrement parameter instead of assuming a fixed 1 second.
final void Function(Duration elapsedIncrement)? onTick;
final VoidCallback? onTimerStart;
final Function(Duration)? onTimerStop; // For data persistence - receives total elapsed duration
final bool isMiniLayout; // Use compact layout for detail tables
final Function(Duration)? onTimerStop;
final Function(Duration)? onWorkSessionComplete;
final bool isMiniLayout;

const AppTimer({
super.key,
this.onTick,
this.onTimerStart,
this.onTimerStop,
this.onWorkSessionComplete,
this.isMiniLayout = false,
});

Expand Down Expand Up @@ -96,7 +101,8 @@ class _AppTimerState extends State<AppTimer> {
bool _isAlarmPlaying = false;
TimerMode _timerMode = TimerMode.pomodoro;
Duration _elapsedTime = const Duration(); // For stopwatch mode
Duration _sessionTotalElapsed = const Duration(); // Total elapsed time for the entire session
Duration _sessionTotalElapsed = const Duration();
Duration _currentWorkSessionElapsed = const Duration(); // Track current work session duration

int _getTotalDurationInSeconds() {
if (_timerMode == TimerMode.normal) {
Expand Down Expand Up @@ -281,10 +287,9 @@ class _AppTimerState extends State<AppTimer> {
void _startTimer() {
if (_isRunning || _isAlarmPlaying) return;

// Reset session total elapsed time for new session
_sessionTotalElapsed = const Duration();
_currentWorkSessionElapsed = const Duration();

// Call the onTimerStart callback if provided
widget.onTimerStart?.call();

if (mounted) {
Expand All @@ -295,7 +300,6 @@ class _AppTimerState extends State<AppTimer> {
_updateSystemTrayTimer();
if (_tickingEnabled) _startTicking();

// Enable wakelock if the setting is enabled
if (_keepScreenAwake) {
_wakelockService.enable();
}
Expand All @@ -311,28 +315,25 @@ class _AppTimerState extends State<AppTimer> {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) return;

// Calculate the actual elapsed time increment for debug vs production
final elapsedIncrement = kDebugMode
? const Duration(minutes: 1) // In debug mode, 1 minute of progress per second
: const Duration(seconds: 1); // In production, 1 second of progress per second
final elapsedIncrement = kDebugMode ? const Duration(minutes: 1) : const Duration(seconds: 1);

setState(() {
if (_timerMode == TimerMode.stopwatch) {
// Stopwatch mode: count up indefinitely
_elapsedTime += elapsedIncrement;
} else {
// Normal and Pomodoro modes: count down
_remainingTime -= elapsedIncrement;
}
});

// Update session total elapsed time
_sessionTotalElapsed += elapsedIncrement;

// Call onTick for UI updates (passes current elapsed/remaining time, not increments)
// Track current work session duration separately
if (_isWorking) {
_currentWorkSessionElapsed += elapsedIncrement;
}

if (widget.onTick != null) {
final timeToDisplay = _timerMode == TimerMode.stopwatch ? _elapsedTime : _remainingTime;
widget.onTick!(timeToDisplay);
widget.onTick!(elapsedIncrement);
}
_updateSystemTrayTimer();

Expand Down Expand Up @@ -386,37 +387,35 @@ class _AppTimerState extends State<AppTimer> {

setState(() {
_isRunning = false;
_isAlarmPlaying = false; // Reset alarm state
_isAlarmPlaying = false;

if (_timerMode == TimerMode.stopwatch) {
_elapsedTime = const Duration(); // Reset stopwatch
_elapsedTime = const Duration();
} else {
if (_timerMode == TimerMode.pomodoro) {
if (!_isWorking) {
// If in break mode, switch back to work mode
_isWorking = true;
}
_completedSessions = 0; // Reset completed sessions
_isLongBreak = false; // Reset long break flag
_completedSessions = 0;
_isLongBreak = false;
}

_remainingTime = Duration(
seconds: _getTimeInSeconds(_workDuration), // Always set to work duration
seconds: _getTimeInSeconds(_workDuration),
);
}

// Stop timer
// Reset work session duration when timer stops
_currentWorkSessionElapsed = Duration.zero;
_timer.cancel();

_soundManagerService.stopAll(); // Stop any playing sounds
_soundManagerService.stopAll();
});

_resetSystemTrayIcon();
_removeTimerMenuItems();
// Reset system tray title/body when stopping timer
_resetSystemTrayToDefault();

// Call the onTimerStop callback with total elapsed duration for the session
if (widget.onTimerStop != null) {
widget.onTimerStop!(_sessionTotalElapsed);
}
Expand All @@ -437,7 +436,6 @@ class _AppTimerState extends State<AppTimer> {
_stopAlarm();

if (_timerMode == TimerMode.stopwatch) {
// In stopwatch mode, just restart the timer
setState(() {
_elapsedTime = const Duration();
});
Expand All @@ -446,36 +444,43 @@ class _AppTimerState extends State<AppTimer> {
}

if (_timerMode == TimerMode.normal) {
// In normal mode, just restart the timer
setState(() {
_remainingTime = Duration(seconds: _getTimeInSeconds(_workDuration));
});
_startTimer();
return;
}

// Pomodoro mode logic
// Pass the current work session duration when work completes
if (_isWorking && _currentWorkSessionElapsed > Duration.zero) {
widget.onWorkSessionComplete?.call(_currentWorkSessionElapsed);
}

setState(() {
if (_isWorking) {
// Work session completed
_completedSessions++;
_isWorking = false;
_isLongBreak = _completedSessions >= _sessionsCount;

if (_isLongBreak) {
_completedSessions = 0; // Reset session count after long break
_completedSessions = 0;
}

_remainingTime = Duration(
seconds: _getTimeInSeconds(_isLongBreak ? _longBreakDuration : _breakDuration),
);

// Reset work session duration when starting break
_currentWorkSessionElapsed = Duration.zero;
} else {
// Break completed, start work
_isWorking = true;
_isLongBreak = false;
_remainingTime = Duration(
seconds: _getTimeInSeconds(_workDuration),
);

// Reset work session duration when starting new work session
_currentWorkSessionElapsed = Duration.zero;
}
});
_startTimer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class TaskUiConstants {
static const int defaultEstimatedTime = 10;
static const List<int> defaultEstimatedTimeOptions = [defaultEstimatedTime, 30, 50, 90, 120];

// Timer auto-save interval
static const int kPeriodicSaveIntervalSeconds = 10;

// Priority Colors & Tooltips
static Color getPriorityColor(EisenhowerPriority? priority) {
switch (priority) {
Expand Down
Loading