Add routines module and full goal management
Routines: workout, meditation and must-do routines with timed, rep and tick steps, sets and rest. A library screen, a builder with templates and drag-to-reorder steps, and a full-screen player that auto-advances timed phases, counts backgrounding and logs broken chains as partial. Goals: a goal editor (deadline, colour, category, default class, archive), routines linked to goals, and a goal field on tasks and routines. Tasks added from inside a goal are now actually attached to it via parentId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
import '../../internal/application/CommitmentClass.dart';
|
||||
import '../../internal/application/RoutineKind.dart';
|
||||
import '../../internal/application/StepMode.dart';
|
||||
import '../../../utils/ObjectConvertors.dart';
|
||||
import 'RoutineStep.dart';
|
||||
|
||||
/// An ordered sequence where the chain, not the step, is the unit of
|
||||
/// completion. Breaking mid-way logs partial.
|
||||
class RoutineChain {
|
||||
@@ -5,6 +11,24 @@ class RoutineChain {
|
||||
|
||||
String title;
|
||||
|
||||
/// The goal this routine serves — "Workout" holds "Full body strength".
|
||||
/// Null for a routine that stands on its own.
|
||||
String? goalId;
|
||||
|
||||
RoutineKind kind;
|
||||
|
||||
/// Weight class, exactly as for a commitment — a non-negotiable morning
|
||||
/// routine carries non-negotiable debt when it breaks.
|
||||
CommitmentClass commitmentClass;
|
||||
|
||||
/// 1..7, Monday first. Empty means unscheduled — run on demand only.
|
||||
List<int> days;
|
||||
|
||||
/// Minutes after midnight the routine is due to start; -1 when unset.
|
||||
int startMinute;
|
||||
|
||||
String note;
|
||||
|
||||
List<RoutineStep> steps;
|
||||
|
||||
/// Index of the step reached when the chain last broke; -1 when clean.
|
||||
@@ -13,19 +37,30 @@ class RoutineChain {
|
||||
RoutineChain({
|
||||
this.id,
|
||||
this.title = "",
|
||||
this.goalId,
|
||||
this.kind = RoutineKind.Workout,
|
||||
this.commitmentClass = CommitmentClass.Standard,
|
||||
List<int>? days,
|
||||
this.startMinute = -1,
|
||||
this.note = "",
|
||||
List<RoutineStep>? steps,
|
||||
this.lastBreakIndex = -1,
|
||||
}) : steps = steps ?? <RoutineStep>[];
|
||||
}) : days = days ?? <int>[],
|
||||
steps = steps ?? <RoutineStep>[];
|
||||
|
||||
factory RoutineChain.fromJson(Map<String, dynamic> json) {
|
||||
return RoutineChain(
|
||||
id: json['id'],
|
||||
title: json['title'] ?? "",
|
||||
steps: json['steps'] == null
|
||||
? <RoutineStep>[]
|
||||
: (json['steps'] as List)
|
||||
.map((item) => RoutineStep.fromJson(item))
|
||||
.toList(),
|
||||
goalId: json['goalId'],
|
||||
kind: getRoutineKind(json['kind']),
|
||||
commitmentClass: getCommitmentClass(json['commitmentClass']),
|
||||
days: json['days'] == null
|
||||
? <int>[]
|
||||
: (json['days'] as List).map((item) => item as int).toList(),
|
||||
startMinute: json['startMinute'] ?? -1,
|
||||
note: json['note'] ?? "",
|
||||
steps: getRoutineStepList(json['steps']),
|
||||
lastBreakIndex: json['lastBreakIndex'] ?? -1,
|
||||
);
|
||||
}
|
||||
@@ -34,6 +69,12 @@ class RoutineChain {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['title'] = title;
|
||||
data['goalId'] = goalId;
|
||||
data['kind'] = kind.name;
|
||||
data['commitmentClass'] = commitmentClass.name;
|
||||
data['days'] = days;
|
||||
data['startMinute'] = startMinute;
|
||||
data['note'] = note;
|
||||
data['steps'] = steps.map((item) => item.toJson()).toList();
|
||||
data['lastBreakIndex'] = lastBreakIndex;
|
||||
return data;
|
||||
@@ -54,44 +95,40 @@ class RoutineChain {
|
||||
bool get complete {
|
||||
return steps.isNotEmpty && completedSteps == steps.length;
|
||||
}
|
||||
}
|
||||
|
||||
class RoutineStep {
|
||||
String? id;
|
||||
|
||||
String title;
|
||||
|
||||
int order;
|
||||
|
||||
int timerSeconds;
|
||||
|
||||
bool completed;
|
||||
|
||||
RoutineStep({
|
||||
this.id,
|
||||
this.title = "",
|
||||
this.order = 0,
|
||||
this.timerSeconds = 0,
|
||||
this.completed = false,
|
||||
});
|
||||
|
||||
factory RoutineStep.fromJson(Map<String, dynamic> json) {
|
||||
return RoutineStep(
|
||||
id: json['id'],
|
||||
title: json['title'] ?? "",
|
||||
order: json['order'] ?? 0,
|
||||
timerSeconds: json['timerSeconds'] ?? 0,
|
||||
completed: json['completed'] ?? false,
|
||||
);
|
||||
bool get scheduled {
|
||||
return days.isNotEmpty;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['title'] = title;
|
||||
data['order'] = order;
|
||||
data['timerSeconds'] = timerSeconds;
|
||||
data['completed'] = completed;
|
||||
return data;
|
||||
bool scheduledOn(DateTime day) {
|
||||
return days.contains(day.weekday);
|
||||
}
|
||||
|
||||
/// Every set of every step, for the "12 sets" figure.
|
||||
int get totalSets {
|
||||
int total = 0;
|
||||
for (RoutineStep step in steps) {
|
||||
total = total + step.sets;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// Planned seconds, rest included. Rep and tick steps count their estimate.
|
||||
int get totalSeconds {
|
||||
int total = 0;
|
||||
for (RoutineStep step in steps) {
|
||||
total = total + step.plannedSeconds;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int get timedSteps {
|
||||
return steps.where((step) => step.mode == StepMode.Timed).length;
|
||||
}
|
||||
|
||||
/// A deep copy for the builder, so cancelling an edit leaves the library
|
||||
/// untouched.
|
||||
RoutineChain copy() {
|
||||
return RoutineChain.fromJson(toJson());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user