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:
alvocool
2026-11-02 08:45:41 +03:00
parent c88887b639
commit 1a5867a98f
49 changed files with 5384 additions and 220 deletions

View File

@@ -0,0 +1,116 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../about/internal/application/TextType.dart';
import '../utils/Colors.dart';
import 'text/Text.dart';
/// A thin circular meter with its value in the middle — the routine row's
/// "how far did I get" dial, and the runner's phase clock ring.
class ProgressRing extends StatelessWidget {
final double fraction;
final double size;
final double stroke;
final Color? fill;
final Color? track;
/// Drawn in the centre. Defaults to the percentage.
final Widget? child;
const ProgressRing({
super.key,
required this.fraction,
this.size = 44,
this.stroke = 3,
this.fill,
this.track,
this.child,
});
@override
Widget build(BuildContext context) {
final double clamped = fraction.isNaN ? 0 : fraction.clamp(0, 1).toDouble();
return SizedBox(
width: size,
height: size,
child: TweenAnimationBuilder<double>(
tween: Tween<double>(end: clamped),
duration: const Duration(milliseconds: 450),
curve: Curves.easeOutCubic,
builder: (context, value, _) {
return CustomPaint(
painter: _RingPainter(
value,
stroke,
fill ?? colorPrimaryDark,
track ?? colorMuted,
),
child: Center(
child: child ??
text("${(clamped * 100).round()}%", size * 0.22,
TextType.Bold,
color: colorPrimaryDark),
),
);
},
),
);
}
}
class _RingPainter extends CustomPainter {
final double fraction;
final double stroke;
final Color fill;
final Color track;
_RingPainter(this.fraction, this.stroke, this.fill, this.track);
@override
void paint(Canvas canvas, Size size) {
final Offset centre = Offset(size.width / 2, size.height / 2);
final double radius = (math.min(size.width, size.height) - stroke) / 2;
final Paint base = Paint()
..style = PaintingStyle.stroke
..strokeWidth = stroke
..color = track;
canvas.drawCircle(centre, radius, base);
if (fraction <= 0) {
return;
}
final Paint arc = Paint()
..style = PaintingStyle.stroke
..strokeWidth = stroke
..strokeCap = StrokeCap.round
..color = fill;
canvas.drawArc(
Rect.fromCircle(center: centre, radius: radius),
-math.pi / 2,
2 * math.pi * fraction,
false,
arc,
);
}
@override
bool shouldRepaint(_RingPainter old) {
return old.fraction != fraction ||
old.fill != fill ||
old.track != track ||
old.stroke != stroke;
}
}