Initial commit: Grounded Flutter frontend
A to-do app that doesn't believe you — an enforcement layer rather than a
neutral ledger.
Architecture ported from Autoreceptives/Frontend/Receptive: stacked MVVM with
the mandatory 4-file screen pattern, one ParentViewModel owning the loading /
network / error overlays and the handleError decision tree, one AppDataManager
gateway, dio comms carrying the three identity headers, secure storage with
random-suffixed keys, and a single-chokepoint Navigator. Package root and Dart
package name are both Grounded; org is nya.
The enforcement engine, one unit per formula in utils/:
- DebtEngine w(class) x severity(d) x decay(t), sublinear severity so old
misses cannot swamp the score; abandonment 2x with 30-day
decay immunity; late complete retains 30%
- StandingEngine Good -> Warned -> Grounded -> Lockdown, derived not set;
Grounded replaces home with the overdue queue
- CapacityEngine blocks over-scheduling against p50 of historically completed
minutes, with a learned per-category estimation multiplier
- IntegrityEngine session integrity, weekly volume, plyometric contact ceiling
and enforced recovery gaps
- ExcuseAnalyser on-device excuse clustering plus the confrontation copy
- GuardrailEngine distress detection and rationed amnesty
- ToneEngine all enforcement copy, so the tone cap lives in one place
CommitmentEvent is append-only and is the source of truth rather than the
status field, which is what makes honest history and excuse analysis possible.
Goals contain commitments via parentId, and a task can be run from a
full-screen runner that derives elapsed time from wall-clock so screen-off
cannot lose time. Backgrounding pauses the clock and is counted. The runner is
mirrored into an ongoing notification, with alarm-class full-screen intents
reserved for non-negotiables.
Design language, fonts, icon and native splash are in place; Mason bricks are
retargeted to this project and verified end-to-end.
flutter analyze lib/ reports no errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mfu2gQLSFN21YRBcU2NrTt
This commit is contained in:
310
frontend/lib/Grounded/see/goal/GoalDetailState.dart
Normal file
310
frontend/lib/Grounded/see/goal/GoalDetailState.dart
Normal file
@@ -0,0 +1,310 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../about/external/data/Commitment.dart';
|
||||
import '../../about/external/data/Goal.dart';
|
||||
import '../../about/internal/application/CommitmentClass.dart';
|
||||
import '../../about/internal/application/CommitmentStatus.dart';
|
||||
import '../../about/internal/application/ProofType.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/text/Text.dart';
|
||||
import '../../utils/Colors.dart';
|
||||
import '../../utils/CommonUtils.dart';
|
||||
import '../../utils/DebtEngine.dart';
|
||||
import '../commitment/NewCommitment.dart';
|
||||
import '../live/LiveTask.dart';
|
||||
import 'ConnectGoalDetail.dart';
|
||||
import 'GoalDetail.dart';
|
||||
import 'ViewGoalDetail.dart';
|
||||
|
||||
class GoalDetailState extends State<GoalDetail>
|
||||
implements ConnectGoalDetail {
|
||||
ViewGoalDetail? _model;
|
||||
|
||||
Goal _goal = Goal();
|
||||
|
||||
List<Commitment> _tasks = <Commitment>[];
|
||||
|
||||
bool _changed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewGoalDetail>.reactive(
|
||||
viewModelBuilder: () => ViewGoalDetail(context, this),
|
||||
onViewModelReady: (viewModel) {
|
||||
_model = viewModel;
|
||||
_initiate();
|
||||
},
|
||||
builder: (context, viewModel, child) => LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Responsive(
|
||||
mobile: _mobileView(constraints),
|
||||
tablet: _mobileView(constraints),
|
||||
desktop: _mobileView(constraints),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initiate() {
|
||||
setState(() {
|
||||
_goal = widget.goal;
|
||||
});
|
||||
_model?.loadTasks(widget.goal);
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────
|
||||
|
||||
void _onBack() {
|
||||
Navigator.pop(context, _changed);
|
||||
}
|
||||
|
||||
void _onStartTask(Commitment task) {
|
||||
_model?.startTask(task);
|
||||
}
|
||||
|
||||
void _onAddTask() async {
|
||||
final result = await GroundedNavigation()
|
||||
.navigateToPageWithData(const NewCommitment(), context);
|
||||
|
||||
if (result == true) {
|
||||
_changed = true;
|
||||
_model?.loadTasks(_goal);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Views ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _mobileView(BoxConstraints constraints) {
|
||||
final List<Commitment> open = _tasks
|
||||
.where((task) =>
|
||||
task.status != CommitmentStatus.Completed &&
|
||||
task.status != CommitmentStatus.LateCompleted &&
|
||||
task.status != CommitmentStatus.Abandoned)
|
||||
.toList();
|
||||
|
||||
final List<Commitment> done = _tasks
|
||||
.where((task) =>
|
||||
task.status == CommitmentStatus.Completed ||
|
||||
task.status == CommitmentStatus.LateCompleted)
|
||||
.toList();
|
||||
|
||||
return Sheet(
|
||||
eyebrow: "Goal",
|
||||
title: _goal.title,
|
||||
onBack: _onBack,
|
||||
action: chromeAction(CupertinoIcons.add, _onAddTask),
|
||||
banner: _progressBanner(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_goal.description.isNotEmpty) ...[
|
||||
text(_goal.description, 15, TextType.Regular,
|
||||
color: colorGrey2, height: 1.6),
|
||||
const SizedBox(height: 28),
|
||||
],
|
||||
sectionBreak("To do", caption: "${open.length} open"),
|
||||
if (open.isEmpty)
|
||||
emptyState(
|
||||
CupertinoIcons.square_list,
|
||||
"Nothing scheduled",
|
||||
"Add the actual sessions — Monday shoulders, Wednesday legs — and they start counting.",
|
||||
)
|
||||
else
|
||||
...open.map(_taskRow),
|
||||
if (done.isNotEmpty) ...[
|
||||
const SizedBox(height: 28),
|
||||
sectionBreak("Done", caption: "${done.length}"),
|
||||
...done.map(_doneRow),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
roundedCornerButton("Add a task", _onAddTask,
|
||||
icon: CupertinoIcons.add),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _progressBanner() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
text("PROGRESS", 9, TextType.Bold,
|
||||
color: colorWhite.withValues(alpha: 0.45),
|
||||
letterSpacing: 1.2),
|
||||
text("${(_goal.progress * 100).round()}%", 13, TextType.Bold,
|
||||
color: colorWhite),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
meter(
|
||||
_goal.progress,
|
||||
fill: colorWhite,
|
||||
track: colorWhite.withValues(alpha: 0.14),
|
||||
height: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A task row leads with the action: the point of opening a goal is to start
|
||||
/// something, not to admire the list.
|
||||
Widget _taskRow(Commitment task) {
|
||||
final Color accent = classColor(task.commitmentClass);
|
||||
|
||||
final bool late = task.windowClosed;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 3,
|
||||
height: 38,
|
||||
margin: const EdgeInsets.only(right: 14, top: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: accent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text(task.title, 16, TextType.Medium,
|
||||
color: colorPrimaryDark,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 7),
|
||||
Row(
|
||||
children: [
|
||||
text(formatWindow(task), 11, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
const SizedBox(width: 9),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorGrey, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 9),
|
||||
text(formatMinutes(task.estMinutes), 11,
|
||||
TextType.Regular, color: colorGrey2),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
pill(proofLabel(task.proofType), colorGrey2, colorMuted,
|
||||
textSize: 9),
|
||||
],
|
||||
),
|
||||
if (late) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
pill(overdueLabel(task), colorStandingGrounded,
|
||||
colorStandingGroundedBg, textSize: 9),
|
||||
const SizedBox(width: 6),
|
||||
pill("−${formatDebt(DebtEngine.commitmentDebt(task))}",
|
||||
colorGrey2, colorMuted, textSize: 9),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
roundedCornerButton(
|
||||
"Start",
|
||||
() => _onStartTask(task),
|
||||
icon: CupertinoIcons.play_fill,
|
||||
verticalPadding: 13,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _doneRow(Commitment task) {
|
||||
final bool late = task.status == CommitmentStatus.LateCompleted;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: card(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
late
|
||||
? CupertinoIcons.checkmark_circle
|
||||
: CupertinoIcons.checkmark_circle_fill,
|
||||
size: 17,
|
||||
color: late ? colorStandingWarned : colorPositive,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: text(task.title, 13, TextType.Regular,
|
||||
color: colorGrey2,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
if (late)
|
||||
pill("Late", colorStandingWarned, colorStandingWarnedBg,
|
||||
textSize: 9),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── ConnectGoalDetail ─────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onGoalLoaded(Goal goal, List<Commitment> tasks) {
|
||||
setState(() {
|
||||
_goal = goal;
|
||||
_tasks = tasks;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onTaskReady(Commitment task) async {
|
||||
// The runner takes over the whole screen — a task you are running is the
|
||||
// thing you are doing, not a row in a list.
|
||||
final result = await GroundedNavigation().navigateToPageWithData(
|
||||
LiveTask(commitment: task, goalTitle: _goal.title),
|
||||
context,
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
_changed = true;
|
||||
_model?.loadTasks(_goal);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user