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:
127
frontend/lib/Grounded/designs/GoalPicker.dart
Normal file
127
frontend/lib/Grounded/designs/GoalPicker.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../about/external/data/Goal.dart';
|
||||
import '../about/internal/application/TextType.dart';
|
||||
import '../utils/Colors.dart';
|
||||
import '../utils/CommonUtils.dart';
|
||||
import 'text/Text.dart';
|
||||
|
||||
/// The goal chooser, shared by the task form and the routine builder. Opened
|
||||
/// only once the goals have been loaded — `selectField` loads on tap.
|
||||
///
|
||||
/// Resolves to the chosen goal, to a bare `Goal()` (no id) for "no goal", or
|
||||
/// to null when dismissed without a choice.
|
||||
Future<Goal?> showGoalPicker(
|
||||
BuildContext context,
|
||||
List<Goal> goals, {
|
||||
String? selectedId,
|
||||
}) {
|
||||
return showModalBottomSheet<Goal>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: colorBlack.withValues(alpha: 0.55),
|
||||
builder: (BuildContext sheetContext) {
|
||||
final List<Goal> active = goals.where((goal) => !goal.archived).toList();
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(sheetContext).size.height * 0.75,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: colorSheetBackground,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(28),
|
||||
topRight: Radius.circular(28),
|
||||
),
|
||||
),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||
children: [
|
||||
text("BELONGS TO", 9, TextType.Bold,
|
||||
color: colorGrey2, letterSpacing: 1.2),
|
||||
const SizedBox(height: 8),
|
||||
text("Which goal is this for?", 24, TextType.Light,
|
||||
color: colorPrimaryDark),
|
||||
const SizedBox(height: 18),
|
||||
_goalOption(
|
||||
sheetContext,
|
||||
Goal(),
|
||||
"No goal",
|
||||
"Stands on its own",
|
||||
colorGrey,
|
||||
selectedId == null,
|
||||
),
|
||||
...active.map((goal) => _goalOption(
|
||||
sheetContext,
|
||||
goal,
|
||||
goal.title,
|
||||
goal.targetDate == null
|
||||
? "${goal.remainingTasks} open"
|
||||
: "${goal.remainingTasks} open · ${targetLabel(goal.targetDate)}",
|
||||
goalColor(goal.colourHex),
|
||||
goal.id == selectedId,
|
||||
)),
|
||||
if (active.isEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
text("No goals yet. Create one from the Goals screen.", 12,
|
||||
TextType.Regular,
|
||||
color: colorGrey2),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _goalOption(
|
||||
BuildContext sheetContext,
|
||||
Goal goal,
|
||||
String title,
|
||||
String caption,
|
||||
Color accent,
|
||||
bool selected,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pop(sheetContext, goal),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.fromLTRB(14, 13, 14, 13),
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: selected ? accent.withValues(alpha: 0.5) : colorBorder,
|
||||
width: selected ? 1.5 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(color: accent, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text(title, 14, TextType.Medium, color: colorPrimaryDark),
|
||||
const SizedBox(height: 3),
|
||||
text(caption, 11, TextType.Regular, color: colorGrey2),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (selected)
|
||||
Icon(CupertinoIcons.checkmark_alt, size: 17, color: accent),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user