Skip to content

Conversation

@ahmet-cetinkaya
Copy link
Owner

@ahmet-cetinkaya ahmet-cetinkaya commented Dec 10, 2025

💪 What

  • Adds periodic task time saving functionality that automatically saves elapsed time every 10 seconds during timer sessions
  • Implements time tracking since last save to ensure accurate duration records
  • Improves data persistence for task time tracking by saving at regular intervals rather than only at session completion
  • Refactors timer component to track and save time records periodically during active sessions

🤔 Why

  • Prevents data loss if the app crashes or closes unexpectedly during timer sessions
  • Improves user experience by ensuring time tracking data is continuously preserved
  • Enhances reliability of the time tracking feature by implementing auto-save functionality
  • Reduces risk of losing valuable productivity data due to unexpected app termination

👀 Usage

The periodic time saving works automatically in the background when using the timer feature:

  1. Start a timer for any task as usual
  2. The system now automatically saves elapsed time every 10 seconds
  3. Continue using the timer normally - no changes to user interaction
  4. Time records are preserved even if the app is unexpectedly closed

👩‍🔬 How to validate

  1. Open any task and start the timer using the play button
  2. Monitor the timer running for at least 30 seconds to observe multiple save intervals
  3. Force-close the app (or simulate a crash) while timer is running
  4. Reopen the app and navigate to the same task
  5. Verify that the elapsed time has been saved and is not lost
  6. Check that time records show accurate duration tracking with 10-second granularity
  7. Repeat the test with different timer modes (Pomodoro, Normal, Stopwatch) to ensure consistent behavior

Summary by Sourcery

Implement periodic autosaving of task timer progress across marathon and task detail views to improve time tracking reliability.

New Features:

  • Add background time accumulation and autosave every 10 seconds while timers are running in the marathon page and task details views.
  • Persist remaining unsaved elapsed time when timers stop or Pomodoro work sessions complete.

Enhancements:

  • Extend AppTimer with callbacks for periodic ticks and work-session completion to support incremental time persistence.
  • Simplify and clean up timer-related UI code by removing redundant comments and minor annotations.

…vals

Add functionality to automatically save elapsed time every 10 seconds
during timer sessions, improving data persistence for task time tracking.
The timer now tracks time since last save and persists duration records
at regular intervals rather than only at session completion.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ahmet-cetinkaya, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the application's timer functionality by implementing periodic time saving. Previously, time was only saved at the session's completion, risking data loss if the app crashed. Now, elapsed time is automatically saved every 10 seconds, ensuring continuous preservation of productivity data and a more robust user experience.

Highlights

  • Periodic Time Saving: Implemented automatic saving of elapsed timer sessions every 10 seconds to prevent data loss during unexpected app closures.
  • Enhanced Data Persistence: Time tracking data is now continuously preserved at regular intervals, significantly improving the reliability of the time tracking feature.
  • Refactored Timer Logic: The core timer component and its integration points have been updated to track and save time records periodically during active sessions, rather than only at session completion.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@ahmet-cetinkaya ahmet-cetinkaya marked this pull request as ready for review December 10, 2025 00:13
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 10, 2025

Reviewer's Guide

Implements periodic autosaving of task timer progress every 10 seconds in marathon and task detail views by tracking unsaved elapsed time, wiring timer callbacks, and persisting incremental durations instead of only full sessions.

Sequence diagram for periodic autosave in marathon timer

sequenceDiagram
  actor User
  participant MarathonPageState
  participant AppTimer
  participant AsyncErrorHandler
  participant Mediator

  User->>MarathonPageState: tap_start_timer()
  MarathonPageState->>AppTimer: onTimerStart()
  AppTimer-->>MarathonPageState: onTimerStart callback

  loop every_second_while_running
    AppTimer->>MarathonPageState: onTick(displayTime)
    MarathonPageState->>MarathonPageState: _handleTimerTick(displayTime)
    MarathonPageState->>MarathonPageState: _timeSinceLastSave += 1_second
    alt timeSinceLastSave >= 10_seconds
      MarathonPageState->>MarathonPageState: _saveElapsedTime(_timeSinceLastSave)
      MarathonPageState->>AsyncErrorHandler: executeVoid(AddTaskTimeRecordCommand)
      AsyncErrorHandler->>Mediator: send(AddTaskTimeRecordCommand)
      Mediator-->>AsyncErrorHandler: command_completed
      AsyncErrorHandler-->>MarathonPageState: result
      MarathonPageState->>MarathonPageState: _timeSinceLastSave = Duration_zero
    end
  end

  User->>AppTimer: stop_timer()
  AppTimer-->>MarathonPageState: onTimerStop(totalElapsed)
  MarathonPageState->>MarathonPageState: _handleTimerStop(totalElapsed)
  alt unsaved_time_remaining
    MarathonPageState->>MarathonPageState: _saveElapsedTime(_timeSinceLastSave)
    MarathonPageState->>AsyncErrorHandler: executeVoid(AddTaskTimeRecordCommand)
    AsyncErrorHandler->>Mediator: send(AddTaskTimeRecordCommand)
    Mediator-->>AsyncErrorHandler: command_completed
    AsyncErrorHandler-->>MarathonPageState: result
    MarathonPageState->>MarathonPageState: _timeSinceLastSave = Duration_zero
  end
Loading

Sequence diagram for periodic autosave in task details timer

sequenceDiagram
  actor User
  participant TaskDetailsContentState
  participant AppTimer
  participant Mediator
  participant TasksService

  User->>TaskDetailsContentState: open_task_details()
  TaskDetailsContentState->>AppTimer: create_AppTimer(isMiniLayout=true, onTick, onTimerStop, onWorkSessionComplete)

  User->>AppTimer: start_timer()

  loop every_second_while_running
    AppTimer->>TaskDetailsContentState: onTick(displayTime)
    TaskDetailsContentState->>TaskDetailsContentState: _handleTimerTick(displayTime)
    TaskDetailsContentState->>TaskDetailsContentState: _timeSinceLastSave += 1_second
    alt timeSinceLastSave >= 10_seconds
      TaskDetailsContentState->>TaskDetailsContentState: _saveTaskTime(_timeSinceLastSave)
      TaskDetailsContentState->>Mediator: send(AddTaskTimeRecordCommand)
      Mediator-->>TaskDetailsContentState: command_completed
      TaskDetailsContentState->>TasksService: notifyTaskUpdated(taskId)
      TaskDetailsContentState->>TaskDetailsContentState: _timeSinceLastSave = Duration_zero
    end
  end

  User->>AppTimer: stop_timer_or_work_session_completes
  AppTimer-->>TaskDetailsContentState: onTimerStop(totalElapsed) or onWorkSessionComplete(totalElapsed)
  TaskDetailsContentState->>TaskDetailsContentState: _onTaskTimerStop(totalElapsed)
  alt unsaved_time_remaining
    TaskDetailsContentState->>TaskDetailsContentState: _saveTaskTime(_timeSinceLastSave)
    TaskDetailsContentState->>Mediator: send(AddTaskTimeRecordCommand)
    Mediator-->>TaskDetailsContentState: command_completed
    TaskDetailsContentState->>TasksService: notifyTaskUpdated(taskId)
    TaskDetailsContentState->>TaskDetailsContentState: _timeSinceLastSave = Duration_zero
  end
Loading

Updated class diagram for AppTimer and timer consumers

classDiagram

  class AppTimer {
    +Function Duration onTick
    +VoidCallback onTimerStart
    +Function Duration onTimerStop
    +Function Duration onWorkSessionComplete
    +bool isMiniLayout
    -Timer _timer
    -bool _isRunning
    -bool _isAlarmPlaying
    -TimerMode _timerMode
    -Duration _elapsedTime
    -Duration _sessionTotalElapsed
    +_startTimer()
    +_stopTimer()
    +_resetTimer()
  }

  class MarathonPageState {
    -TaskListItem _selectedTask
    -bool _isTimerRunning
    -bool _isDimmed
    -Timer _dimmingTimer
    -Duration _timeSinceLastSave
    +bool wantKeepAlive
    +_onTimerStart()
    +_handleTimerTick(Duration displayTime)
    +_saveElapsedTime(Duration elapsed)
    +_handleTimerStop(Duration totalElapsed)
    +_startDimmingTimer()
    +_stopDimmingTimer()
  }

  class TaskDetailsContentState {
    -Task _task
    -Set~String~ _visibleOptionalFields
    -Duration _timeSinceLastSave
    +_handleTimerTick(Duration displayTime)
    +_saveTaskTime(Duration elapsed)
    +_onTaskTimerStop(Duration totalElapsed)
  }

  class AddTaskTimeRecordCommand {
    +String taskId
    +int duration
    +DateTime customDateTime
  }

  class Mediator {
    +send(AddTaskTimeRecordCommand command)
  }

  class TasksService {
    +notifyTaskUpdated(String taskId)
  }

  MarathonPageState --> AppTimer : uses
  TaskDetailsContentState --> AppTimer : uses

  MarathonPageState --> AddTaskTimeRecordCommand : creates
  TaskDetailsContentState --> AddTaskTimeRecordCommand : creates

  MarathonPageState --> Mediator : sends_command_via_AsyncErrorHandler
  TaskDetailsContentState --> Mediator : sends_command

  TaskDetailsContentState --> TasksService : notifyTaskUpdated

  AppTimer --> MarathonPageState : callback_onTick
  AppTimer --> MarathonPageState : callback_onTimerStop
  AppTimer --> MarathonPageState : callback_onWorkSessionComplete

  AppTimer --> TaskDetailsContentState : callback_onTick
  AppTimer --> TaskDetailsContentState : callback_onTimerStop
  AppTimer --> TaskDetailsContentState : callback_onWorkSessionComplete
Loading

File-Level Changes

Change Details Files
Track and periodically persist elapsed task time during marathon timer sessions.
  • Introduce _timeSinceLastSave state to accumulate elapsed time between saves in the marathon page state
  • Handle per-second timer ticks via a new _handleTimerTick that increments the accumulator and triggers a save every 10 seconds
  • Refactor timer stop handling to flush any remaining unsaved time on stop or work-session completion
  • Create a reusable _saveElapsedTime helper that posts AddTaskTimeRecordCommand only when a task is selected and elapsed time is positive
  • Wire AppTimer callbacks in the marathon view to use onTick for periodic updates and onWorkSessionComplete to share stop handling logic
src/lib/presentation/ui/features/tasks/pages/marathon_page.dart
Extend AppTimer to distinguish periodic ticks, full timer stops, and Pomodoro work-session completion for better time recording semantics.
  • Add an optional onWorkSessionComplete callback to AppTimer to signal the end of a Pomodoro work block separately from overall timer stop
  • Maintain _sessionTotalElapsed across ticks and pass it to onTimerStop and onWorkSessionComplete where appropriate
  • Invoke onWorkSessionComplete when finishing a Pomodoro work session before transitioning to break/next session, guarding against zero-duration sessions
  • Clean up comments and minor logic around timer start/stop, reset, and system tray/wakelock handling without changing behavior
src/lib/presentation/ui/features/tasks/components/timer.dart
Add periodic autosave for task time from the task details view using per-second ticks and a 10-second threshold.
  • Add a _timeSinceLastSave accumulator to TaskDetailsContentState to track elapsed work time since last persistence
  • Handle AppTimer onTick to increment the accumulator every second and call a new _saveTaskTime helper every 10 seconds
  • Refactor _onTaskTimerStop to flush any remaining unsaved time via _saveTaskTime instead of saving the full session duration
  • Implement _saveTaskTime to guard against missing task id or non-positive duration and to send AddTaskTimeRecordCommand plus task update notifications
src/lib/presentation/ui/features/tasks/components/task_details_content.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ahmet-cetinkaya ahmet-cetinkaya self-assigned this Dec 10, 2025
@ahmet-cetinkaya ahmet-cetinkaya added the bug Something isn't working label Dec 10, 2025
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • The new periodic save logic in both MarathonPage and TaskDetailsContent assumes each tick represents one real second (_timeSinceLastSave += const Duration(seconds: 1)), but AppTimer advances time by 1 minute per second in kDebugMode; consider basing the accumulation on the actual increment (e.g., by passing the increment or total elapsed into the callback) so autosave intervals and durations remain accurate in all modes.
  • In Pomodoro mode, onWorkSessionComplete currently receives _sessionTotalElapsed, which is only reset in _startTimer and will accumulate across multiple work/break cycles; if the intent is to persist each Pomodoro work block as a separate entry, you may want to track and pass a per-work-session elapsed duration instead of the full session total.
  • The 10-second autosave interval is hard-coded in multiple places (e.g., _timeSinceLastSave.inSeconds >= 10); consider extracting this into a shared constant to avoid drift if the interval needs to change later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new periodic save logic in both `MarathonPage` and `TaskDetailsContent` assumes each tick represents one real second (`_timeSinceLastSave += const Duration(seconds: 1)`), but `AppTimer` advances time by 1 minute per second in `kDebugMode`; consider basing the accumulation on the actual increment (e.g., by passing the increment or total elapsed into the callback) so autosave intervals and durations remain accurate in all modes.
- In Pomodoro mode, `onWorkSessionComplete` currently receives `_sessionTotalElapsed`, which is only reset in `_startTimer` and will accumulate across multiple work/break cycles; if the intent is to persist each Pomodoro work block as a separate entry, you may want to track and pass a per-work-session elapsed duration instead of the full session total.
- The 10-second autosave interval is hard-coded in multiple places (e.g., `_timeSinceLastSave.inSeconds >= 10`); consider extracting this into a shared constant to avoid drift if the interval needs to change later.

## Individual Comments

### Comment 1
<location> `src/lib/presentation/ui/features/tasks/pages/marathon_page.dart:133-135` </location>
<code_context>

-  // Timer event handlers
-  void _onTaskTimerStop(Duration totalElapsed) {
+  void _handleTimerTick(Duration displayTime) {
+    _timeSinceLastSave += const Duration(seconds: 1);
+    if (_timeSinceLastSave.inSeconds >= 10) {
+      _saveTaskTime(_timeSinceLastSave);
+      _timeSinceLastSave = Duration.zero;
</code_context>

<issue_to_address>
**issue (bug_risk):** Timer tick handler assumes 1s real-time increments, which diverges from AppTimer’s accelerated debug timing.

In debug builds, `AppTimer` advances by 1 minute per tick (`kDebugMode ? Duration(minutes: 1) : Duration(seconds: 1)`), but `_handleTimerTick` always adds `Duration(seconds: 1)` and uses a fixed 10-second threshold. This causes persisted time to be much smaller than the logical elapsed time in debug, and the effective chunk size to vary by build mode. Prefer either deriving the delta from `displayTime` or exposing the timer’s tick duration from `AppTimer` so this code doesn’t hard-code 1-second steps.
</issue_to_address>

### Comment 2
<location> `src/lib/presentation/ui/features/tasks/pages/marathon_page.dart:135-144` </location>
<code_context>
     _startDimmingTimer();
   }

-  Future<void> _handleTimerStop(Duration totalElapsed) async {
-    _stopDimmingTimer();
+  void _handleTimerTick(Duration displayTime) {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Using the same stop handler for `onTimerStop` and `onWorkSessionComplete` also stops dimming between Pomodoro work/break cycles.

Here `_handleTimerStop` (which calls `_stopDimmingTimer`) is used for both `onTimerStop` and `onWorkSessionComplete`. In Pomodoro mode, `onWorkSessionComplete` fires when work ends but the timer immediately continues into a break. If dimming should persist across work→break transitions, these two callbacks need different handlers: one to handle work-segment completion (e.g., flush accumulated time) and another to handle a true timer stop that also cancels dimming.

Suggested implementation:

```
    setState(() {
      _isTimerRunning = true;
      _timeSinceLastSave = Duration.zero;
    });
    _startDimmingTimer();
  }

  void _handleTimerTick(Duration displayTime) {
    _timeSinceLastSave += const Duration(seconds: 1);
    if (_timeSinceLastSave.inSeconds >= 10) {
      _saveElapsedTime(_timeSinceLastSave);
      _timeSinceLastSave = Duration.zero;
    }
  }

  /// Called when a work session completes (e.g., Pomodoro work → break transition).
  /// Flushes any accumulated elapsed time but intentionally does NOT stop dimming,
  /// so the dimming persists into the break segment.
  Future<void> _handleWorkSessionComplete(Duration totalElapsed) async {
    if (_timeSinceLastSave > Duration.zero) {
      await _saveElapsedTime(_timeSinceLastSave);
      _timeSinceLastSave = Duration.zero;
    }
  }

  /// Called when the timer actually stops (user stops / session ends).
  /// Flushes accumulated elapsed time and stops the dimming timer.
  Future<void> _handleTimerStop(Duration totalElapsed) async {
    _stopDimmingTimer();

    if (_timeSinceLastSave > Duration.zero) {
      await _saveElapsedTime(_timeSinceLastSave);
      _timeSinceLastSave = Duration.zero;
    }
  }

  Future<void> _saveElapsedTime(Duration elapsed) async {
    if (_selectedTask == null) return;

```

To fully implement the separation of concerns between work-session completion and true timer stop, you should also:
1. Update the timer configuration where the callbacks are wired (likely where the Pomodoro/marathon timer is created/initialized in this same file):
   - Set `onTimerStop` (or equivalent) to `_handleTimerStop`.
   - Set `onWorkSessionComplete` (or equivalent) to `_handleWorkSessionComplete`.
   - Keep the tick handler (`onTick` / `onTimerTick`) pointing to `_handleTimerTick`.
2. Ensure any previous use of `_handleTimerStop` as a shared handler for both events is replaced so that only true stop events (not work→break transitions) call `_handleTimerStop`. This will prevent dimming from being stopped between Pomodoro work/break cycles.
</issue_to_address>

### Comment 3
<location> `src/lib/presentation/ui/features/tasks/pages/marathon_page.dart:136-128` </location>
<code_context>
+  void _handleTimerTick(Duration displayTime) {
+    _timeSinceLastSave += const Duration(seconds: 1);
+    if (_timeSinceLastSave.inSeconds >= 10) {
+      _saveElapsedTime(_timeSinceLastSave);
+      _timeSinceLastSave = Duration.zero;
+    }
+  }
</code_context>

<issue_to_address>
**question (bug_risk):** Unsaved accumulated time is attributed to whichever task is currently selected, which can shift when changing tasks mid-session.

`_saveElapsedTime` uses `_selectedTask` at save time, not when the elapsed chunk was earned. If the user switches tasks while the timer is running, any pending `_timeSinceLastSave` is logged against the new task, not the one that was active. If this isn’t intentional, consider flushing pending time before updating `_selectedTask`, or tracking elapsed time per active-task interval instead of globally.
</issue_to_address>

### Comment 4
<location> `src/lib/presentation/ui/features/tasks/components/timer.dart:29-37` </location>
<code_context>
-  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,
   });
</code_context>

<issue_to_address>
**suggestion (bug_risk):** `onWorkSessionComplete` receives the total session elapsed time, not the duration of the just-finished work segment.

In `_handlePomodoroTimerCompletion`, `onWorkSessionComplete` is called with `_sessionTotalElapsed`, which is only reset in `_startTimer`. This means it accumulates time across both work and break segments, so callers can’t derive the duration of the just-finished work session. If the intent is to persist per-work-session data, consider passing the work-segment duration instead, or providing both total-session and current-segment durations.
</issue_to_address>

### Comment 5
<location> `src/lib/presentation/ui/features/tasks/components/timer.dart:310-319` </location>
<code_context>
     _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** `onTick` provides a logical time value while consumers assume a fixed 1s per tick, which breaks under debug acceleration.

Because `elapsedIncrement` is 1 minute in debug and 1 second in release, any consumer that increments its own counter by a hard-coded 1s per tick will drift from the actual elapsed time. Since `MarathonPage` and `TaskDetailsContent` both do this, consider documenting that `onTick` reflects logical elapsed time and either (a) passing the increment to consumers, or (b) exposing an API to read the effective increment or accumulated elapsed time so callers don’t assume a fixed delta.

Suggested implementation:

```
    if (mounted) {
      _updateSystemTrayTimer();
      if (_tickingEnabled) _startTicking();

      if (_keepScreenAwake) {
        _wakelockService.enable();
      }

      // NOTE: onTick observers receive a *logical* elapsed time increment.
      // In debug this is 1 minute per tick, in release it is 1 second per tick.
      // Callers MUST NOT assume a fixed 1s delta; instead, they should use the
      // provided elapsedIncrement to update any local counters.
      _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
        if (!mounted) return;

        final elapsedIncrement = kDebugMode
            ? const Duration(minutes: 1)
            : const Duration(seconds: 1);

        setState(() {
          if (_timerMode == TimerMode.stopwatch) {

```

To fully implement the recommendation of passing the logical increment to consumers and avoiding hard-coded 1s assumptions, you should also:

1. Update the `onTick` API in this timer widget:
   - Change its type from a no-arg callback to accepting the elapsed increment, for example:
     - from `final VoidCallback? onTick;`
     - to `final void Function(Duration elapsedIncrement)? onTick;`
   - Update every invocation inside `timer.dart` from `widget.onTick?.call();` (or similar) to `widget.onTick?.call(elapsedIncrement);` so consumers receive the logical delta on each tick.
   - Add documentation to the `onTick` field (or constructor parameter) explaining that:
     - It represents **logical elapsed time**, not wall-clock time.
     - In debug, 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 per tick.

2. Update all consumers (such as `MarathonPage` and `TaskDetailsContent`) that currently:
   - Implement an `onTick` callback with no parameters, and
   - Increment their own counters by a hard-coded `1` second per tick.
   They should instead:
   - Accept a `Duration elapsedIncrement` parameter, and
   - Increment their timers by `elapsedIncrement` (or use an accumulated value exposed by the timer widget, if you introduce one).

3. Optionally, if other parts of the codebase need to know the effective increment without subscribing to `onTick`, expose a read-only getter on the timer widget or its `State`, e.g.:
   - `Duration get logicalTickIncrement => kDebugMode ? const Duration(minutes: 1) : const Duration(seconds: 1);`
   and then use that instead of hard-coding `Duration(seconds: 1)` anywhere callers need the increment.

These adjustments will ensure that caller logic stays aligned with the logical elapsed time in both debug and release modes, and will prevent drift caused by assuming a fixed 1-second tick.
</issue_to_address>

### Comment 6
<location> `src/lib/presentation/ui/features/tasks/components/task_details_content.dart:1404-1406` </location>
<code_context>
           ),
           child: AppTimer(
             isMiniLayout: true,
+            onTick: _handleTimerTick,
             onTimerStop: _onTaskTimerStop,
+            onWorkSessionComplete: _onTaskTimerStop,
           ),
         ),
</code_context>

<issue_to_address>
**issue (bug_risk):** Task details timer persistence has the same 1s-per-tick assumption and potential Pomodoro phase ambiguity as the marathon page.

`_handleTimerTick` always adds `Duration(seconds: 1)` to `_timeSinceLastSave` and doesn’t distinguish work vs break. With `onWorkSessionComplete` also calling `_onTaskTimerStop`, this can (1) desync persisted duration from actual elapsed time in debug, and (2) persist break time to the task if the timer runs during breaks. To align with the timer semantics, base the increment on the real elapsed delta (as in `MarathonPage`) and, if needed, only count work-phase time toward the persisted value.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces a periodic auto-save mechanism for task timers, saving accumulated time every 10 seconds instead of only when the timer stops. This involved adding a _timeSinceLastSave state variable, implementing _handleTimerTick and _saveTaskTime (or _saveElapsedTime) methods in TaskDetailsContentState and _MarathonPageState, and updating the AppTimer widget to expose onTick and onWorkSessionComplete callbacks. Additionally, many redundant comments were removed from the AppTimer widget. The review comments suggest extracting the hardcoded 10-second periodic save interval into a shared named constant, kPeriodicSaveIntervalSeconds, for improved readability and maintainability.

- Extract kPeriodicSaveIntervalSeconds constant to remove magic numbers
- Fix debug mode timing by using actual elapsed increments from timer
- Separate work session complete from timer stop handlers to prevent dimming issues
- Flush pending elapsed time before task switching to ensure correct attribution
- Update onTick API to pass elapsed increment instead of display time
- Add comprehensive documentation for timer callback behavior
- Add _currentWorkSessionElapsed to track work segment duration separately
- Update _toggleWorkBreak to pass actual work session duration instead of total session
- Reset work session duration appropriately when starting/stopping work sessions
- Fixes issue where break time was included in work session duration
@ahmet-cetinkaya ahmet-cetinkaya merged commit 8f33e4b into main Dec 10, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants