import '../../external/data/RoutineChain.dart'; import '../../external/data/RoutineStep.dart'; import 'RoutinePhase.dart'; /// A routine being run. Like [LiveSession], every clock is derived from /// wall-clock rather than counted by a ticker, so a screen-off period cannot /// lose or invent time. class RoutineRun { final RoutineChain chain; final List phases; int phaseIndex = 0; final DateTime startedAt; /// Banked foreground seconds for the whole run. int _runSeconds = 0; /// Banked foreground seconds for the current phase. int _phaseSeconds = 0; /// Start of the active stretch; null while paused. DateTime? resumedAt; /// Times the app was left mid-run. Shown afterwards, never hidden. int backgroundedCount = 0; /// Phases skipped forward without being done. int skippedCount = 0; RoutineRun(this.chain) : phases = buildPhases(chain), startedAt = DateTime.now(); /// Each set becomes a work phase; rest follows every set that has rest, /// except after the final set of the final step. static List buildPhases(RoutineChain chain) { final List result = []; for (int index = 0; index < chain.steps.length; index++) { final RoutineStep step = chain.steps[index]; final int sets = step.sets < 1 ? 1 : step.sets; for (int set = 1; set <= sets; set++) { result.add(RoutinePhase(stepIndex: index, step: step, set: set)); final bool finalBeat = index == chain.steps.length - 1 && set == sets; if (step.restSeconds > 0 && !finalBeat) { result.add( RoutinePhase(stepIndex: index, step: step, set: set, rest: true)); } } } return result; } RoutinePhase get phase { return phases[phaseIndex]; } bool get running { return resumedAt != null; } bool get onLastPhase { return phaseIndex >= phases.length - 1; } int _live(DateTime? now) { if (resumedAt == null) { return 0; } return (now ?? DateTime.now()).difference(resumedAt!).inSeconds; } int elapsedSeconds({DateTime? now}) { return _runSeconds + _live(now); } int phaseElapsed({DateTime? now}) { return _phaseSeconds + _live(now); } int phaseRemaining({DateTime? now}) { final int remaining = phase.seconds - phaseElapsed(now: now); return remaining > 0 ? remaining : 0; } /// 0..1 through the current phase; 0 for phases that wait for a tap. double phaseProgress({DateTime? now}) { if (!phase.countsDown) { return 0; } final double value = phaseElapsed(now: now) / phase.seconds; return value > 1 ? 1 : value; } bool phaseFinished({DateTime? now}) { return phase.countsDown && phaseElapsed(now: now) >= phase.seconds; } /// Seconds left in the whole run, by plan — elapsed work in this phase /// counts, phases still ahead count at their planned length. int plannedRemaining({DateTime? now}) { int total = phase.countsDown ? phaseRemaining(now: now) : 0; for (int index = phaseIndex + 1; index < phases.length; index++) { total = total + phases[index].seconds; } return total; } void pause({DateTime? now}) { if (resumedAt == null) { return; } final int live = _live(now); _runSeconds = _runSeconds + live; _phaseSeconds = _phaseSeconds + live; resumedAt = null; } void resume({DateTime? now}) { if (resumedAt != null) { return; } resumedAt = now ?? DateTime.now(); } /// Bank the current phase and start the next. Returns false at the end. /// A step is marked complete when its last set's work phase is done. bool advance({bool skipped = false, DateTime? now}) { final DateTime moment = now ?? DateTime.now(); final bool wasRunning = running; if (!phase.rest && phase.lastSet && !skipped) { phase.step.completed = true; } if (skipped && !phase.rest) { skippedCount = skippedCount + 1; } pause(now: moment); _phaseSeconds = 0; if (onLastPhase) { return false; } phaseIndex = phaseIndex + 1; if (wasRunning) { resume(now: moment); } return true; } /// Back to the start of the previous work phase. Rest is never a target. void back({DateTime? now}) { final DateTime moment = now ?? DateTime.now(); final bool wasRunning = running; pause(now: moment); _phaseSeconds = 0; int target = phaseIndex - 1; while (target > 0 && phases[target].rest) { target = target - 1; } phaseIndex = target < 0 ? 0 : target; phases[phaseIndex].step.completed = false; if (wasRunning) { resume(now: moment); } } /// Jump to the first set of [stepIndex]. void jumpTo(int stepIndex, {DateTime? now}) { final int target = phases.indexWhere((item) => item.stepIndex == stepIndex && !item.rest); if (target < 0) { return; } final DateTime moment = now ?? DateTime.now(); final bool wasRunning = running; pause(now: moment); _phaseSeconds = 0; phaseIndex = target; if (wasRunning) { resume(now: moment); } } /// The step index the chain broke at, for a partial log; -1 when clean. int get breakIndex { return chain.complete ? -1 : phase.stepIndex; } /// The next work phase after this one, for the "up next" line. RoutinePhase? get nextWork { for (int index = phaseIndex + 1; index < phases.length; index++) { if (!phases[index].rest) { return phases[index]; } } return null; } }