Progress gives a Day/Week/Month view of routines held against a weekly target, compared with the same point of the previous period, with active time and the daily streak. Targets & reminders sets routines and minutes per week and schedules a heads-up before each routine. The runner's summary now shows today's held count and streak. Finished runs are mirrored to a device run log that feeds both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
164 lines
5.5 KiB
Dart
164 lines
5.5 KiB
Dart
import '../about/external/data/RoutineChain.dart';
|
|
import '../about/internal/application/ProgressPeriod.dart';
|
|
import '../about/internal/application/ProgressTargets.dart';
|
|
import '../about/internal/application/RoutineRunRecord.dart';
|
|
|
|
/// Progress over time, derived from the device's run log. Pure functions:
|
|
/// the screen asks, this answers, nothing is stored.
|
|
class ProgressEngine {
|
|
static DateTime dayOf(DateTime value) =>
|
|
DateTime(value.year, value.month, value.day);
|
|
|
|
/// Weeks start on Monday, matching routine `days` (1 = Monday).
|
|
static DateTime weekStart(DateTime value) =>
|
|
dayOf(value).subtract(Duration(days: value.weekday - 1));
|
|
|
|
static DateTime monthStart(DateTime value) =>
|
|
DateTime(value.year, value.month);
|
|
|
|
/// The start of the period containing [now], and of the one before it.
|
|
static DateTime periodStart(ProgressPeriod period, DateTime now) {
|
|
switch (period) {
|
|
case ProgressPeriod.Day:
|
|
return dayOf(now);
|
|
case ProgressPeriod.Week:
|
|
return weekStart(now);
|
|
case ProgressPeriod.Month:
|
|
return monthStart(now);
|
|
}
|
|
}
|
|
|
|
static DateTime periodEnd(ProgressPeriod period, DateTime start) {
|
|
switch (period) {
|
|
case ProgressPeriod.Day:
|
|
return start.add(const Duration(days: 1));
|
|
case ProgressPeriod.Week:
|
|
return start.add(const Duration(days: 7));
|
|
case ProgressPeriod.Month:
|
|
return DateTime(start.year, start.month + 1);
|
|
}
|
|
}
|
|
|
|
static DateTime previousStart(ProgressPeriod period, DateTime start) {
|
|
switch (period) {
|
|
case ProgressPeriod.Day:
|
|
return start.subtract(const Duration(days: 1));
|
|
case ProgressPeriod.Week:
|
|
return start.subtract(const Duration(days: 7));
|
|
case ProgressPeriod.Month:
|
|
return DateTime(start.year, start.month - 1);
|
|
}
|
|
}
|
|
|
|
/// Routine slots the schedule asks for each week.
|
|
static int scheduledPerWeek(List<RoutineChain> routines) {
|
|
int count = 0;
|
|
for (RoutineChain routine in routines) {
|
|
count = count + routine.days.length;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static List<RoutineRunRecord> between(
|
|
List<RoutineRunRecord> records, DateTime start, DateTime end) {
|
|
return records
|
|
.where((record) =>
|
|
!record.startedAt.isBefore(start) && record.startedAt.isBefore(end))
|
|
.toList();
|
|
}
|
|
|
|
static int held(List<RoutineRunRecord> records) =>
|
|
records.where((record) => record.held).length;
|
|
|
|
static int minutes(List<RoutineRunRecord> records) {
|
|
int seconds = 0;
|
|
for (RoutineRunRecord record in records) {
|
|
seconds = seconds + record.seconds;
|
|
}
|
|
return (seconds / 60).round();
|
|
}
|
|
|
|
/// The weekly targets scaled to the period: a day is a seventh of a week,
|
|
/// a month is its days over seven.
|
|
static int routineTarget(
|
|
ProgressTargets targets, ProgressPeriod period, DateTime start) {
|
|
return _scale(targets.routinesPerWeek, period, start);
|
|
}
|
|
|
|
static int minuteTarget(
|
|
ProgressTargets targets, ProgressPeriod period, DateTime start) {
|
|
return _scale(targets.minutesPerWeek, period, start);
|
|
}
|
|
|
|
static int _scale(int weekly, ProgressPeriod period, DateTime start) {
|
|
switch (period) {
|
|
case ProgressPeriod.Day:
|
|
return (weekly / 7).ceil();
|
|
case ProgressPeriod.Week:
|
|
return weekly;
|
|
case ProgressPeriod.Month:
|
|
final int days = periodEnd(period, start).difference(start).inDays;
|
|
return (weekly * days / 7).round();
|
|
}
|
|
}
|
|
|
|
/// Held routines per bucket: days of the week, or weeks of the month.
|
|
/// A day has no buckets; it lists its runs instead.
|
|
static List<int> buckets(
|
|
List<RoutineRunRecord> records, ProgressPeriod period, DateTime start) {
|
|
switch (period) {
|
|
case ProgressPeriod.Day:
|
|
return <int>[];
|
|
case ProgressPeriod.Week:
|
|
return List<int>.generate(7, (index) {
|
|
final DateTime day = start.add(Duration(days: index));
|
|
return held(between(records, day, day.add(const Duration(days: 1))));
|
|
});
|
|
case ProgressPeriod.Month:
|
|
final DateTime end = periodEnd(period, start);
|
|
final List<int> result = <int>[];
|
|
DateTime week = weekStart(start);
|
|
while (week.isBefore(end)) {
|
|
final DateTime from = week.isBefore(start) ? start : week;
|
|
final DateTime next = week.add(const Duration(days: 7));
|
|
result.add(held(between(records, from, next.isAfter(end) ? end : next)));
|
|
week = next;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// Which bucket [now] falls in, so the chart can highlight it.
|
|
static int currentBucket(ProgressPeriod period, DateTime start, DateTime now) {
|
|
switch (period) {
|
|
case ProgressPeriod.Day:
|
|
return 0;
|
|
case ProgressPeriod.Week:
|
|
return now.weekday - 1;
|
|
case ProgressPeriod.Month:
|
|
return dayOf(now).difference(weekStart(start)).inDays ~/ 7;
|
|
}
|
|
}
|
|
|
|
/// Consecutive days with at least one held routine, ending today — or
|
|
/// yesterday, so a streak is not shown as broken before the day is out.
|
|
static int streak(List<RoutineRunRecord> records, DateTime now) {
|
|
final Set<DateTime> days = records
|
|
.where((record) => record.held)
|
|
.map((record) => dayOf(record.startedAt))
|
|
.toSet();
|
|
|
|
DateTime cursor = dayOf(now);
|
|
if (!days.contains(cursor)) {
|
|
cursor = cursor.subtract(const Duration(days: 1));
|
|
}
|
|
|
|
int count = 0;
|
|
while (days.contains(cursor)) {
|
|
count = count + 1;
|
|
cursor = cursor.subtract(const Duration(days: 1));
|
|
}
|
|
return count;
|
|
}
|
|
}
|