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,9 +1,13 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../about/external/data/Commitment.dart';
|
||||
import '../about/external/data/RoutineStep.dart';
|
||||
import '../about/internal/application/CommitmentClass.dart';
|
||||
import '../about/internal/application/RoutineKind.dart';
|
||||
import '../about/internal/application/Standing.dart';
|
||||
import '../about/internal/application/StepMode.dart';
|
||||
import 'Colors.dart';
|
||||
|
||||
/// Formatting and small shared derivations. Anything a screen would otherwise
|
||||
@@ -186,3 +190,165 @@ String hourLabel(int hour) {
|
||||
}
|
||||
return "${hour - 12}pm";
|
||||
}
|
||||
|
||||
Color kindColor(RoutineKind value) {
|
||||
switch (value) {
|
||||
case RoutineKind.Workout:
|
||||
return colorKindWorkout;
|
||||
case RoutineKind.Meditation:
|
||||
return colorKindMeditation;
|
||||
case RoutineKind.MustDo:
|
||||
return colorKindMustDo;
|
||||
}
|
||||
}
|
||||
|
||||
Color kindBackground(RoutineKind value) {
|
||||
switch (value) {
|
||||
case RoutineKind.Workout:
|
||||
return colorKindWorkoutBg;
|
||||
case RoutineKind.Meditation:
|
||||
return colorKindMeditationBg;
|
||||
case RoutineKind.MustDo:
|
||||
return colorKindMustDoBg;
|
||||
}
|
||||
}
|
||||
|
||||
/// The dark end of the kind's gradient — featured cards and the runner stage.
|
||||
Color kindDeep(RoutineKind value) {
|
||||
switch (value) {
|
||||
case RoutineKind.Workout:
|
||||
return colorKindWorkoutDeep;
|
||||
case RoutineKind.Meditation:
|
||||
return colorKindMeditationDeep;
|
||||
case RoutineKind.MustDo:
|
||||
return colorKindMustDoDeep;
|
||||
}
|
||||
}
|
||||
|
||||
IconData kindIcon(RoutineKind value) {
|
||||
switch (value) {
|
||||
case RoutineKind.Workout:
|
||||
return Icons.fitness_center_rounded;
|
||||
case RoutineKind.Meditation:
|
||||
return Icons.self_improvement_rounded;
|
||||
case RoutineKind.MustDo:
|
||||
return CupertinoIcons.checkmark_seal_fill;
|
||||
}
|
||||
}
|
||||
|
||||
IconData stepModeIcon(StepMode value) {
|
||||
switch (value) {
|
||||
case StepMode.Timed:
|
||||
return CupertinoIcons.timer;
|
||||
case StepMode.Reps:
|
||||
return CupertinoIcons.repeat;
|
||||
case StepMode.Check:
|
||||
return CupertinoIcons.checkmark_square;
|
||||
}
|
||||
}
|
||||
|
||||
/// "45s", "2m", "1m 30s" — for step durations, where a clock face would be
|
||||
/// noise.
|
||||
String formatSeconds(int seconds) {
|
||||
final int safe = seconds < 0 ? 0 : seconds;
|
||||
if (safe < 60) {
|
||||
return "${safe}s";
|
||||
}
|
||||
final int minutes = safe ~/ 60;
|
||||
final int remainder = safe % 60;
|
||||
if (remainder == 0) {
|
||||
return "${minutes}m";
|
||||
}
|
||||
return "${minutes}m ${remainder}s";
|
||||
}
|
||||
|
||||
/// "07:30" from minutes after midnight; empty when unset.
|
||||
String formatStartMinute(int minute) {
|
||||
if (minute < 0) {
|
||||
return "";
|
||||
}
|
||||
final int hour = minute ~/ 60;
|
||||
final int rest = minute % 60;
|
||||
return "${hour.toString().padLeft(2, '0')}:${rest.toString().padLeft(2, '0')}";
|
||||
}
|
||||
|
||||
/// "M T W" style day initials for a 1..7 index.
|
||||
String weekdayInitial(int weekday) {
|
||||
const List<String> initials = <String>["M", "T", "W", "T", "F", "S", "S"];
|
||||
if (weekday < 1 || weekday > 7) {
|
||||
return "";
|
||||
}
|
||||
return initials[weekday - 1];
|
||||
}
|
||||
|
||||
/// "Weekdays", "Every day", "Mon · Wed · Fri" — a schedule read at a glance.
|
||||
String formatDays(List<int> days) {
|
||||
if (days.isEmpty) {
|
||||
return "On demand";
|
||||
}
|
||||
final List<int> sorted = List<int>.from(days)..sort();
|
||||
if (sorted.length == 7) {
|
||||
return "Every day";
|
||||
}
|
||||
if (sorted.length == 5 && sorted.every((day) => day <= 5)) {
|
||||
return "Weekdays";
|
||||
}
|
||||
if (sorted.length == 2 && sorted.contains(6) && sorted.contains(7)) {
|
||||
return "Weekends";
|
||||
}
|
||||
return sorted.map((day) => weekdayName(day).substring(0, 3)).join(" · ");
|
||||
}
|
||||
|
||||
/// "4 × 12 reps · rest 90s" — one line that says what a step asks of you.
|
||||
String stepSummary(RoutineStep step) {
|
||||
final List<String> parts = <String>[];
|
||||
|
||||
switch (step.mode) {
|
||||
case StepMode.Timed:
|
||||
parts.add(step.sets > 1
|
||||
? "${step.sets} × ${formatSeconds(step.timerSeconds)}"
|
||||
: formatSeconds(step.timerSeconds));
|
||||
break;
|
||||
case StepMode.Reps:
|
||||
parts.add("${step.sets} × ${step.reps} reps");
|
||||
break;
|
||||
case StepMode.Check:
|
||||
parts.add(step.timerSeconds > 0
|
||||
? "Tick · ~${formatSeconds(step.timerSeconds)}"
|
||||
: "Tick");
|
||||
break;
|
||||
}
|
||||
|
||||
if (step.restSeconds > 0) {
|
||||
parts.add("rest ${formatSeconds(step.restSeconds)}");
|
||||
}
|
||||
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
/// A goal's accent, falling back to ink when it has none.
|
||||
Color goalColor(String colourHex) {
|
||||
if (colourHex.isEmpty) {
|
||||
return colorPrimaryDark;
|
||||
}
|
||||
return getColorFromHex(colourHex);
|
||||
}
|
||||
|
||||
/// "41 days left", "Due today", "12 days over" — the deadline read as a
|
||||
/// distance, which is what actually changes behaviour.
|
||||
String targetLabel(DateTime? target) {
|
||||
if (target == null) {
|
||||
return "";
|
||||
}
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime today = DateTime(now.year, now.month, now.day);
|
||||
final DateTime day = DateTime(target.year, target.month, target.day);
|
||||
final int days = day.difference(today).inDays;
|
||||
if (days == 0) {
|
||||
return "Due today";
|
||||
}
|
||||
if (days < 0) {
|
||||
return "${-days} day${days == -1 ? "" : "s"} over";
|
||||
}
|
||||
return "$days day${days == 1 ? "" : "s"} left";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user