Add Progress over time and a targets and reminders setup

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
This commit is contained in:
alvocool
2026-11-08 20:17:58 +03:00
parent 4b71257429
commit add94f621c
26 changed files with 1590 additions and 4 deletions

View File

@@ -0,0 +1,13 @@
/// The span the Progress screen measures.
enum ProgressPeriod { Day, Week, Month }
String periodLabel(ProgressPeriod value) {
switch (value) {
case ProgressPeriod.Day:
return "Day";
case ProgressPeriod.Week:
return "Week";
case ProgressPeriod.Month:
return "Month";
}
}

View File

@@ -0,0 +1,46 @@
/// What a good week looks like, set by the user on the Targets screen.
/// Progress measures against these; reminders fire before scheduled routines.
class ProgressTargets {
/// Routines held (every step done) per week.
int routinesPerWeek;
/// Minutes spent inside routines per week, held or not.
int minutesPerWeek;
bool reminders;
/// How long before a routine's start the reminder fires.
int reminderLead;
/// False until the user has saved the Targets screen once, so Progress can
/// ask for a target instead of silently using the defaults.
bool set;
ProgressTargets({
this.routinesPerWeek = 5,
this.minutesPerWeek = 150,
this.reminders = false,
this.reminderLead = 10,
this.set = false,
});
factory ProgressTargets.fromJson(Map<String, dynamic> json) {
return ProgressTargets(
routinesPerWeek: json['routinesPerWeek'] ?? 5,
minutesPerWeek: json['minutesPerWeek'] ?? 150,
reminders: json['reminders'] ?? false,
reminderLead: json['reminderLead'] ?? 10,
set: json['set'] ?? false,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['routinesPerWeek'] = routinesPerWeek;
data['minutesPerWeek'] = minutesPerWeek;
data['reminders'] = reminders;
data['reminderLead'] = reminderLead;
data['set'] = set;
return data;
}
}

View File

@@ -0,0 +1,56 @@
/// One finished run of a routine, kept on the device so Progress can draw the
/// days and weeks without waiting on the network. The server keeps its own
/// log through `Routine/Log`; this is the local mirror.
class RoutineRunRecord {
String routineId;
String title;
/// [RoutineKind] name.
String kind;
DateTime startedAt;
int seconds;
int completedSteps;
int totalSteps;
RoutineRunRecord({
required this.routineId,
required this.title,
required this.kind,
required this.startedAt,
this.seconds = 0,
this.completedSteps = 0,
this.totalSteps = 0,
});
/// Every step, in order. Partial runs still count toward active time.
bool get held => totalSteps > 0 && completedSteps >= totalSteps;
factory RoutineRunRecord.fromJson(Map<String, dynamic> json) {
return RoutineRunRecord(
routineId: json['routineId'] ?? '',
title: json['title'] ?? '',
kind: json['kind'] ?? '',
startedAt: DateTime.tryParse(json['startedAt'] ?? '') ?? DateTime.now(),
seconds: json['seconds'] ?? 0,
completedSteps: json['completedSteps'] ?? 0,
totalSteps: json['totalSteps'] ?? 0,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['routineId'] = routineId;
data['title'] = title;
data['kind'] = kind;
data['startedAt'] = startedAt.toIso8601String();
data['seconds'] = seconds;
data['completedSteps'] = completedSteps;
data['totalSteps'] = totalSteps;
return data;
}
}