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:
116
frontend/lib/Grounded/designs/ProgressRing.dart
Normal file
116
frontend/lib/Grounded/designs/ProgressRing.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user