Skip to content

Commit 8f33e4b

Browse files
Merge pull request #153 from ahmet-cetinkaya/fix/timer-auto-save-refactor
feat(timer): implement periodic task time saving with 10-second intervals
2 parents 7531ef5 + 6da2a61 commit 8f33e4b

File tree

4 files changed

+129
-75
lines changed

4 files changed

+129
-75
lines changed

src/lib/presentation/ui/features/tasks/components/task_details_content.dart

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class TaskDetailsContentState extends State<TaskDetailsContent> {
9090

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

9495
// Define optional field keys
9596
static const String keyTags = 'tags';
@@ -1400,22 +1401,51 @@ class TaskDetailsContentState extends State<TaskDetailsContent> {
14001401
),
14011402
child: AppTimer(
14021403
isMiniLayout: true,
1404+
onTick: _handleTimerTick,
14031405
onTimerStop: _onTaskTimerStop,
1406+
onWorkSessionComplete: _onTaskWorkSessionComplete,
14041407
),
14051408
),
14061409
);
14071410

1408-
// Timer event handlers
1409-
void _onTaskTimerStop(Duration totalElapsed) {
1411+
void _handleTimerTick(Duration elapsedIncrement) {
1412+
// Use the elapsed increment provided by the timer
1413+
_timeSinceLastSave += elapsedIncrement;
1414+
if (_timeSinceLastSave.inSeconds >= TaskUiConstants.kPeriodicSaveIntervalSeconds) {
1415+
_saveTaskTime(_timeSinceLastSave);
1416+
_timeSinceLastSave = Duration.zero;
1417+
}
1418+
}
1419+
1420+
void _saveTaskTime(Duration elapsed) {
14101421
if (!mounted) return;
14111422
if (_task?.id == null) return;
1423+
if (elapsed.inSeconds <= 0) return;
14121424

1413-
// Only save if there's actual time elapsed
1414-
if (totalElapsed.inSeconds > 0) {
1415-
final command =
1416-
AddTaskTimeRecordCommand(duration: totalElapsed.inSeconds, taskId: _task!.id, customDateTime: DateTime.now());
1417-
_mediator.send(command);
1418-
_tasksService.notifyTaskUpdated(_task!.id);
1425+
final command = AddTaskTimeRecordCommand(
1426+
duration: elapsed.inSeconds,
1427+
taskId: _task!.id,
1428+
customDateTime: DateTime.now(),
1429+
);
1430+
_mediator.send(command);
1431+
_tasksService.notifyTaskUpdated(_task!.id);
1432+
}
1433+
1434+
/// Called when a work session completes (e.g., Pomodoro work → break transition).
1435+
/// Flushes any accumulated elapsed time for the current task.
1436+
void _onTaskWorkSessionComplete(Duration totalElapsed) {
1437+
if (_timeSinceLastSave > Duration.zero) {
1438+
_saveTaskTime(_timeSinceLastSave);
1439+
_timeSinceLastSave = Duration.zero;
1440+
}
1441+
}
1442+
1443+
/// Called when the timer actually stops (user stops / session ends).
1444+
/// Flushes accumulated elapsed time for the current task.
1445+
void _onTaskTimerStop(Duration totalElapsed) {
1446+
if (_timeSinceLastSave > Duration.zero) {
1447+
_saveTaskTime(_timeSinceLastSave);
1448+
_timeSinceLastSave = Duration.zero;
14191449
}
14201450
}
14211451
}

src/lib/presentation/ui/features/tasks/components/timer.dart

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,21 @@ import 'package:whph/presentation/ui/shared/services/abstraction/i_translation_s
2323
import 'package:whph/presentation/ui/shared/utils/app_theme_helper.dart';
2424

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

3135
const AppTimer({
3236
super.key,
3337
this.onTick,
3438
this.onTimerStart,
3539
this.onTimerStop,
40+
this.onWorkSessionComplete,
3641
this.isMiniLayout = false,
3742
});
3843

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

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

284-
// Reset session total elapsed time for new session
285290
_sessionTotalElapsed = const Duration();
291+
_currentWorkSessionElapsed = const Duration();
286292

287-
// Call the onTimerStart callback if provided
288293
widget.onTimerStart?.call();
289294

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

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

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

319320
setState(() {
320321
if (_timerMode == TimerMode.stopwatch) {
321-
// Stopwatch mode: count up indefinitely
322322
_elapsedTime += elapsedIncrement;
323323
} else {
324-
// Normal and Pomodoro modes: count down
325324
_remainingTime -= elapsedIncrement;
326325
}
327326
});
328327

329-
// Update session total elapsed time
330328
_sessionTotalElapsed += elapsedIncrement;
331329

332-
// Call onTick for UI updates (passes current elapsed/remaining time, not increments)
330+
// Track current work session duration separately
331+
if (_isWorking) {
332+
_currentWorkSessionElapsed += elapsedIncrement;
333+
}
334+
333335
if (widget.onTick != null) {
334-
final timeToDisplay = _timerMode == TimerMode.stopwatch ? _elapsedTime : _remainingTime;
335-
widget.onTick!(timeToDisplay);
336+
widget.onTick!(elapsedIncrement);
336337
}
337338
_updateSystemTrayTimer();
338339

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

387388
setState(() {
388389
_isRunning = false;
389-
_isAlarmPlaying = false; // Reset alarm state
390+
_isAlarmPlaying = false;
390391

391392
if (_timerMode == TimerMode.stopwatch) {
392-
_elapsedTime = const Duration(); // Reset stopwatch
393+
_elapsedTime = const Duration();
393394
} else {
394395
if (_timerMode == TimerMode.pomodoro) {
395396
if (!_isWorking) {
396-
// If in break mode, switch back to work mode
397397
_isWorking = true;
398398
}
399-
_completedSessions = 0; // Reset completed sessions
400-
_isLongBreak = false; // Reset long break flag
399+
_completedSessions = 0;
400+
_isLongBreak = false;
401401
}
402402

403403
_remainingTime = Duration(
404-
seconds: _getTimeInSeconds(_workDuration), // Always set to work duration
404+
seconds: _getTimeInSeconds(_workDuration),
405405
);
406406
}
407407

408-
// Stop timer
408+
// Reset work session duration when timer stops
409+
_currentWorkSessionElapsed = Duration.zero;
409410
_timer.cancel();
410411

411-
_soundManagerService.stopAll(); // Stop any playing sounds
412+
_soundManagerService.stopAll();
412413
});
413414

414415
_resetSystemTrayIcon();
415416
_removeTimerMenuItems();
416-
// Reset system tray title/body when stopping timer
417417
_resetSystemTrayToDefault();
418418

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

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

448446
if (_timerMode == TimerMode.normal) {
449-
// In normal mode, just restart the timer
450447
setState(() {
451448
_remainingTime = Duration(seconds: _getTimeInSeconds(_workDuration));
452449
});
453450
_startTimer();
454451
return;
455452
}
456453

457-
// Pomodoro mode logic
454+
// Pass the current work session duration when work completes
455+
if (_isWorking && _currentWorkSessionElapsed > Duration.zero) {
456+
widget.onWorkSessionComplete?.call(_currentWorkSessionElapsed);
457+
}
458+
458459
setState(() {
459460
if (_isWorking) {
460-
// Work session completed
461461
_completedSessions++;
462462
_isWorking = false;
463463
_isLongBreak = _completedSessions >= _sessionsCount;
464464

465465
if (_isLongBreak) {
466-
_completedSessions = 0; // Reset session count after long break
466+
_completedSessions = 0;
467467
}
468468

469469
_remainingTime = Duration(
470470
seconds: _getTimeInSeconds(_isLongBreak ? _longBreakDuration : _breakDuration),
471471
);
472+
473+
// Reset work session duration when starting break
474+
_currentWorkSessionElapsed = Duration.zero;
472475
} else {
473-
// Break completed, start work
474476
_isWorking = true;
475477
_isLongBreak = false;
476478
_remainingTime = Duration(
477479
seconds: _getTimeInSeconds(_workDuration),
478480
);
481+
482+
// Reset work session duration when starting new work session
483+
_currentWorkSessionElapsed = Duration.zero;
479484
}
480485
});
481486
_startTimer();

src/lib/presentation/ui/features/tasks/constants/task_ui_constants.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class TaskUiConstants {
3737
static const int defaultEstimatedTime = 10;
3838
static const List<int> defaultEstimatedTimeOptions = [defaultEstimatedTime, 30, 50, 90, 120];
3939

40+
// Timer auto-save interval
41+
static const int kPeriodicSaveIntervalSeconds = 10;
42+
4043
// Priority Colors & Tooltips
4144
static Color getPriorityColor(EisenhowerPriority? priority) {
4245
switch (priority) {

0 commit comments

Comments
 (0)