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,23 +1,19 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../about/external/data/Goal.dart';
|
||||
import '../../about/external/initial/GoalRequest.dart';
|
||||
import '../../about/internal/application/CommitmentClass.dart';
|
||||
import '../../about/internal/application/TextType.dart';
|
||||
import '../../configs/Navigator.dart';
|
||||
import '../../designs/Component.dart';
|
||||
import '../../designs/Responsive.dart';
|
||||
import '../../designs/Shell.dart';
|
||||
import '../../designs/buttons/Buttons.dart';
|
||||
import '../../designs/input/InputFields.dart';
|
||||
import '../../designs/text/Text.dart';
|
||||
import '../../utils/Colors.dart';
|
||||
import '../../utils/CommonUtils.dart';
|
||||
import '../../utils/Validators.dart';
|
||||
import 'ConnectGoals.dart';
|
||||
import 'GoalDetail.dart';
|
||||
import 'GoalEditor.dart';
|
||||
import 'Goals.dart';
|
||||
import 'ViewGoals.dart';
|
||||
|
||||
@@ -63,124 +59,13 @@ class GoalsState extends State<Goals> implements ConnectGoals {
|
||||
}
|
||||
}
|
||||
|
||||
void _onNewGoal() {
|
||||
_openGoalSheet();
|
||||
}
|
||||
void _onNewGoal() async {
|
||||
final result = await GroundedNavigation()
|
||||
.navigateToPageWithData(const GoalEditor(), context);
|
||||
|
||||
/// Goals are lightweight on purpose — a name and a default class. The
|
||||
/// weight lives on the tasks inside them.
|
||||
void _openGoalSheet() {
|
||||
final TextEditingController title = TextEditingController();
|
||||
final TextEditingController description = TextEditingController();
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
CommitmentClass defaultClass = CommitmentClass.Standard;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: colorPrimaryDark.withValues(alpha: 0.6),
|
||||
builder: (BuildContext sheetContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext sheetContext, StateSetter setSheetState) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorSheetBackground,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(28),
|
||||
topRight: Radius.circular(28),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 32),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: colorGrey.withValues(alpha: 0.35),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
),
|
||||
),
|
||||
text("NEW GOAL", 9, TextType.Bold,
|
||||
color: colorGrey2, letterSpacing: 1.2),
|
||||
const SizedBox(height: 10),
|
||||
text("What are you\nworking toward?", 26,
|
||||
TextType.Light,
|
||||
color: colorPrimaryDark, height: 1.2),
|
||||
const SizedBox(height: 20),
|
||||
inputField(
|
||||
"Goal",
|
||||
title,
|
||||
hint: "Workout, thesis, get the flat sorted…",
|
||||
validator: Validators.title,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
inputField(
|
||||
"Why it matters",
|
||||
description,
|
||||
hint: "Optional, but it helps on the bad days",
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
text("TASKS DEFAULT TO", 9, TextType.Bold,
|
||||
color: colorGrey2, letterSpacing: 1.0),
|
||||
const SizedBox(height: 10),
|
||||
segmentedSelector<CommitmentClass>(
|
||||
options: CommitmentClass.values,
|
||||
selected: defaultClass,
|
||||
label: classLabel,
|
||||
onSelected: (value) => setSheetState(() {
|
||||
defaultClass = value;
|
||||
}),
|
||||
activeColor: classColor(defaultClass),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
roundedCornerButton(
|
||||
"Create goal",
|
||||
() {
|
||||
if (formKey.currentState?.validate() != true) {
|
||||
return;
|
||||
}
|
||||
Navigator.pop(sheetContext);
|
||||
_model?.save(GoalRequest(
|
||||
title: title.text.trim(),
|
||||
description: description.text.trim(),
|
||||
defaultClass: defaultClass.name,
|
||||
startDate: DateTime.now().toIso8601String(),
|
||||
));
|
||||
},
|
||||
icon: CupertinoIcons.add,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: textButton("Cancel",
|
||||
() => Navigator.pop(sheetContext),
|
||||
textSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (result is Goal) {
|
||||
_model?.loadGoals();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _mobileView(BoxConstraints constraints) {
|
||||
@@ -221,7 +106,7 @@ class GoalsState extends State<Goals> implements ConnectGoals {
|
||||
|
||||
Widget _goalCard(Goal goal) {
|
||||
final Color accent =
|
||||
goal.slipping ? colorStandingGrounded : colorPrimaryDark;
|
||||
goal.slipping ? colorStandingGrounded : goalColor(goal.colourHex);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@@ -239,6 +124,35 @@ class GoalsState extends State<Goals> implements ConnectGoals {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: goalColor(goal.colourHex),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
text(
|
||||
[
|
||||
if (goal.category.isNotEmpty) goal.category,
|
||||
goal.targetDate == null
|
||||
? "No deadline"
|
||||
: targetLabel(goal.targetDate),
|
||||
].join(" · ").toUpperCase(),
|
||||
9,
|
||||
TextType.Bold,
|
||||
color: goal.targetDate != null &&
|
||||
goal.targetDate!.isBefore(DateTime.now())
|
||||
? colorStandingGrounded
|
||||
: colorGrey2,
|
||||
letterSpacing: 1.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
text(goal.title, 19, TextType.Light,
|
||||
color: colorPrimaryDark,
|
||||
maxLines: 2,
|
||||
@@ -302,9 +216,4 @@ class GoalsState extends State<Goals> implements ConnectGoals {
|
||||
_goals = goals;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onGoalSaved() {
|
||||
_model?.loadGoals();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user