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

@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stacked/stacked.dart';
import '../../about/internal/application/RoutineRunRecord.dart';
import '../../utils/ProgressEngine.dart';
import '../../about/external/data/RoutineStep.dart';
import '../../about/internal/application/NotificationType.dart';
import '../../about/internal/application/RoutineKind.dart';
@@ -36,6 +38,8 @@ class RoutineRunnerState extends State<RoutineRunner>
implements ConnectRoutineRunner {
ViewRoutineRunner? _model;
List<RoutineRunRecord> _history = <RoutineRunRecord>[];
late RoutineRun _run;
Timer? _ticker;
@@ -107,6 +111,7 @@ class RoutineRunnerState extends State<RoutineRunner>
}
void _initiate() {
_model?.loadHistory();
_startTicker();
_syncBreath();
_model?.publishRun(_run);
@@ -1152,6 +1157,8 @@ class RoutineRunnerState extends State<RoutineRunner>
],
),
),
const SizedBox(height: 10),
_todayLine(held),
const Spacer(),
roundedCornerButton(
"Log it",
@@ -1167,8 +1174,70 @@ class RoutineRunnerState extends State<RoutineRunner>
);
}
/// Where this run leaves the day: routines held today and the daily
/// streak, counting this run as if logged.
Widget _todayLine(bool held) {
final DateTime now = DateTime.now();
final List<RoutineRunRecord> today = ProgressEngine.between(
_history,
ProgressEngine.dayOf(now),
ProgressEngine.dayOf(now).add(const Duration(days: 1)));
final List<RoutineRunRecord> withThis =
List<RoutineRunRecord>.from(_history);
if (held) {
withThis.add(RoutineRunRecord(
routineId: _run.chain.id ?? "",
title: _run.chain.title,
kind: _run.chain.kind.name,
startedAt: now,
completedSteps: 1,
totalSteps: 1,
));
}
final int heldToday = ProgressEngine.held(today) + (held ? 1 : 0);
final int streak = ProgressEngine.streak(withThis, now);
return GlassSurface(
radius: BorderRadius.zero,
tint: colorGlassStrong,
padding: const EdgeInsets.fromLTRB(18, 14, 18, 14),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
text("Today", 14, TextType.Medium, color: colorInk),
const SizedBox(height: 3),
text(
"$heldToday held · streak $streak ${streak == 1 ? 'day' : 'days'}",
12,
TextType.Regular,
color: colorInkSoft,
),
],
),
),
Icon(CupertinoIcons.flame_fill,
size: 20,
color: streak > 0 ? colorOverdueWarm : colorInkSoft),
],
),
);
}
// ── ConnectRoutineRunner ──────────────────────────────────────────────────
@override
void onHistoryLoaded(List<RoutineRunRecord> records) {
setState(() {
_history = records;
});
}
@override
void onLogged(bool held) {
Navigator.pop(context, true);