/// A task actively being run. This is what the full-screen runner and the /// ongoing notification are both driven from. class LiveSession { String commitmentId; String title; String goalTitle; /// Wall-clock start of the run. DateTime startedAt; /// Seconds of *foreground* work accumulated. Backgrounding the app stops /// this accruing — that is the whole point of Timer proof. int accumulatedSeconds; /// When the current active stretch began; null while paused. DateTime? resumedAt; /// Seconds required before the run counts as proof. int requiredSeconds; /// How many times the user left the app mid-run. Surfaced afterwards rather /// than hidden, because leaving repeatedly is the behaviour worth seeing. int backgroundedCount; LiveSession({ this.commitmentId = "", this.title = "", this.goalTitle = "", required this.startedAt, this.accumulatedSeconds = 0, this.resumedAt, this.requiredSeconds = 0, this.backgroundedCount = 0, }); factory LiveSession.fromJson(Map json) { return LiveSession( commitmentId: json['commitmentId'] ?? "", title: json['title'] ?? "", goalTitle: json['goalTitle'] ?? "", startedAt: DateTime.tryParse(json['startedAt'] ?? "") ?? DateTime.now(), accumulatedSeconds: json['accumulatedSeconds'] ?? 0, resumedAt: DateTime.tryParse(json['resumedAt'] ?? ""), requiredSeconds: json['requiredSeconds'] ?? 0, backgroundedCount: json['backgroundedCount'] ?? 0, ); } Map toJson() { final Map data = {}; data['commitmentId'] = commitmentId; data['title'] = title; data['goalTitle'] = goalTitle; data['startedAt'] = startedAt.toIso8601String(); data['accumulatedSeconds'] = accumulatedSeconds; data['resumedAt'] = resumedAt?.toIso8601String(); data['requiredSeconds'] = requiredSeconds; data['backgroundedCount'] = backgroundedCount; return data; } bool get running { return resumedAt != null; } /// Foreground seconds as of now, including the stretch in progress. This is /// derived from wall-clock rather than counted by the ticker, so the value /// stays correct across a screen-off period where timers are throttled. int elapsedSeconds({DateTime? now}) { if (resumedAt == null) { return accumulatedSeconds; } final DateTime moment = now ?? DateTime.now(); return accumulatedSeconds + moment.difference(resumedAt!).inSeconds; } int remainingSeconds({DateTime? now}) { final int remaining = requiredSeconds - elapsedSeconds(now: now); return remaining > 0 ? remaining : 0; } /// 0..1 toward the requirement. double progress({DateTime? now}) { if (requiredSeconds <= 0) { return 0; } final double value = elapsedSeconds(now: now) / requiredSeconds; return value > 1 ? 1 : value; } /// The requirement has been met — completion is now allowed. bool satisfied({DateTime? now}) { if (requiredSeconds <= 0) { return true; } return elapsedSeconds(now: now) >= requiredSeconds; } /// Pause and bank the stretch that just ended. void pause({DateTime? now}) { if (resumedAt == null) { return; } final DateTime moment = now ?? DateTime.now(); accumulatedSeconds = accumulatedSeconds + moment.difference(resumedAt!).inSeconds; resumedAt = null; } void resume({DateTime? now}) { if (resumedAt != null) { return; } resumedAt = now ?? DateTime.now(); } }