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:
alvocool
2026-07-27 09:11:17 +03:00
commit 16bff634b5
315 changed files with 19132 additions and 0 deletions

View File

@@ -0,0 +1,552 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../about/external/data/Commitment.dart';
import '../../about/external/initial/CommitmentRequest.dart';
import '../../about/internal/application/CommitmentClass.dart';
import '../../about/internal/application/EnergyCost.dart';
import '../../about/internal/application/NotificationType.dart';
import '../../about/internal/application/ProofType.dart';
import '../../about/internal/application/TextType.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/CapacityEngine.dart';
import '../../utils/Colors.dart';
import '../../utils/CommonUtils.dart';
import '../../utils/Validators.dart';
import 'ConnectNewCommitment.dart';
import 'NewCommitment.dart';
import 'ViewNewCommitment.dart';
class NewCommitmentState extends State<NewCommitment>
implements ConnectNewCommitment {
ViewNewCommitment? _model;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _title = TextEditingController();
final TextEditingController _category = TextEditingController();
final TextEditingController _estimate = TextEditingController();
CommitmentClass _class = CommitmentClass.Standard;
EnergyCost _energy = EnergyCost.Medium;
ProofType _proof = ProofType.Honour;
DateTime? _windowStart;
DateTime? _windowEnd;
double _multiplier = 1.0;
String _windowError = "";
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ViewNewCommitment>.reactive(
viewModelBuilder: () => ViewNewCommitment(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() {
// A window, not a date: the default opens now and closes in two hours, so
// the field is never left as a bare day.
final DateTime now = DateTime.now();
setState(() {
_windowStart = now;
_windowEnd = now.add(const Duration(hours: 2));
});
}
// ── Handlers ──────────────────────────────────────────────────────────────
void _onBack() {
Navigator.pop(context, false);
}
void _onCategoryChanged(String value) {
_model?.resolveMultiplier(value.trim());
}
void _onClassSelected(CommitmentClass value) {
setState(() {
_class = value;
// Non-negotiables carry real consequences, so they default to real proof
// rather than the honour checkbox.
if (value == CommitmentClass.NonNegotiable &&
_proof == ProofType.Honour) {
_proof = ProofType.Photo;
}
});
}
void _onEnergySelected(EnergyCost value) {
setState(() {
_energy = value;
});
}
void _onProofSelected(ProofType value) {
setState(() {
_proof = value;
});
}
void _onPickWindowStart() async {
final DateTime? picked = await _pickMoment(_windowStart);
if (picked == null) {
return;
}
setState(() {
_windowStart = picked;
if (_windowEnd == null || !_windowEnd!.isAfter(picked)) {
_windowEnd = picked.add(const Duration(hours: 2));
}
_windowError = "";
});
}
void _onPickWindowEnd() async {
final DateTime? picked = await _pickMoment(_windowEnd);
if (picked == null) {
return;
}
setState(() {
_windowEnd = picked;
_windowError = Validators.window(_windowStart, _windowEnd) ?? "";
});
}
Future<DateTime?> _pickMoment(DateTime? initial) async {
final DateTime base = initial ?? DateTime.now();
final DateTime? day = await showDatePicker(
context: context,
initialDate: base,
firstDate: DateTime.now().subtract(const Duration(days: 1)),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (day == null || !mounted) {
return null;
}
final TimeOfDay? time = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(base),
);
if (time == null) {
return null;
}
return DateTime(day.year, day.month, day.day, time.hour, time.minute);
}
void _onSave() {
final String? windowIssue = Validators.window(_windowStart, _windowEnd);
if (windowIssue != null) {
setState(() {
_windowError = windowIssue;
});
return;
}
if (_formKey.currentState?.validate() != true) {
return;
}
_model?.save(_buildRequest(), _buildCandidate());
}
CommitmentRequest _buildRequest() {
return CommitmentRequest(
commitmentClass: _class.name,
title: _title.text.trim(),
category: _category.text.trim(),
dueStart: _windowStart?.toIso8601String() ?? "",
dueEnd: _windowEnd?.toIso8601String() ?? "",
estMinutes: int.tryParse(_estimate.text.trim()) ?? 0,
energy: _energy.name,
proofType: _proof.name,
proofTimerMinutes:
_proof == ProofType.Timer ? int.tryParse(_estimate.text.trim()) ?? 0 : 0,
);
}
Commitment _buildCandidate() {
return Commitment(
commitmentClass: _class,
title: _title.text.trim(),
category: _category.text.trim(),
dueStart: _windowStart,
dueEnd: _windowEnd,
estMinutes: int.tryParse(_estimate.text.trim()) ?? 0,
energy: _energy,
proofType: _proof,
);
}
// ── Views ─────────────────────────────────────────────────────────────────
Widget _mobileView(BoxConstraints constraints) {
final int estimate = int.tryParse(_estimate.text.trim()) ?? 0;
final bool multiplierWorthShowing = _multiplier > 1.15 && estimate > 0;
return Sheet(
eyebrow: "New",
title: "Commitment",
onBack: _onBack,
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
displayTitle("What are you\ncommitting to?"),
const SizedBox(height: 28),
inputField(
"Title",
_title,
hint: "Say it the way you would say it out loud",
validator: Validators.title,
),
const SizedBox(height: 20),
inputField(
"Category",
_category,
hint: "Admin, gym, deep work…",
onChanged: _onCategoryChanged,
),
const SizedBox(height: 28),
sectionBreak("Class", caption: "decides the weight"),
segmentedSelector<CommitmentClass>(
options: CommitmentClass.values,
selected: _class,
label: classLabel,
onSelected: _onClassSelected,
activeColor: classColor(_class),
),
const SizedBox(height: 12),
text(
_classDescription(_class),
12,
TextType.Regular,
color: colorGrey2,
height: 1.5,
),
const SizedBox(height: 28),
sectionBreak("Window", caption: "not just a day"),
if (_windowError.isNotEmpty) ...[
text(_windowError, 11, TextType.Regular, color: colorNegative),
const SizedBox(height: 10),
],
Row(
children: [
Expanded(
child: selectField(
"Opens",
_windowStart == null ? "" : formatDateTime(_windowStart),
_onPickWindowStart,
icon: CupertinoIcons.calendar,
),
),
const SizedBox(width: 10),
Expanded(
child: selectField(
"Closes",
_windowEnd == null ? "" : formatDateTime(_windowEnd),
_onPickWindowEnd,
icon: CupertinoIcons.calendar,
),
),
],
),
const SizedBox(height: 10),
text(
"When this window closes, the item goes overdue. It does not roll over to tomorrow.",
12,
TextType.Regular,
color: colorGrey2,
height: 1.5,
),
const SizedBox(height: 28),
sectionBreak("Effort"),
inputField(
"Estimated minutes",
_estimate,
hint: "How long you think it takes",
validator: Validators.estimateMinutes,
keyboard: TextInputType.number,
onChanged: (value) => setState(() {}),
),
if (multiplierWorthShowing) ...[
const SizedBox(height: 12),
card(
background: colorStandingWarnedBg,
borderColor: colorStandingWarned.withValues(alpha: 0.20),
padding: const EdgeInsets.all(14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(CupertinoIcons.info_circle_fill,
size: 15, color: colorStandingWarned),
const SizedBox(width: 10),
Expanded(
child: text(
"On ${_category.text.trim().isEmpty ? "this category" : _category.text.trim()} you historically take ${_multiplier.toStringAsFixed(1)}× your estimate. Your day is planned against ${formatMinutes(estimate * _multiplier)}.",
12,
TextType.Regular,
color: colorPrimaryDark,
height: 1.5,
),
),
],
),
),
],
const SizedBox(height: 20),
text("ENERGY COST", 9, TextType.Bold,
color: colorGrey2, letterSpacing: 1.0),
const SizedBox(height: 10),
segmentedSelector<EnergyCost>(
options: EnergyCost.values,
selected: _energy,
label: (value) => value.name,
onSelected: _onEnergySelected,
),
const SizedBox(height: 28),
sectionBreak("Proof", caption: "the checkbox is the enemy"),
segmentedSelector<ProofType>(
options: ProofType.values,
selected: _proof,
label: proofLabel,
onSelected: _onProofSelected,
),
const SizedBox(height: 12),
text(
_proofDescription(_proof),
12,
TextType.Regular,
color: colorGrey2,
height: 1.5,
),
const SizedBox(height: 32),
roundedCornerButton(
"Commit",
_onSave,
icon: CupertinoIcons.checkmark,
),
const SizedBox(height: 10),
Center(
child: text(
"You can change the details later. You cannot change the history.",
11,
TextType.Regular,
color: colorGrey2,
align: TextAlign.center,
),
),
],
),
),
);
}
String _classDescription(CommitmentClass value) {
switch (value) {
case CommitmentClass.NonNegotiable:
return "Never deferrable, heaviest debt, hardest escalation. Meds, deadlines, rent.";
case CommitmentClass.Standard:
return "Normal weight, two deferrals, then it is complete or abandon.";
case CommitmentClass.Elective:
return "No debt if you miss it. Auto-archives if it sits untouched.";
}
}
String _proofDescription(ProofType value) {
switch (value) {
case ProofType.Honour:
return "A plain checkbox. Fine for trivia, worthless for anything you actually lie to yourself about.";
case ProofType.Photo:
return "Camera only, no gallery. Timestamped, and near-duplicate photos get flagged.";
case ProofType.Timer:
return "A foreground session. Backgrounding the app pauses the clock.";
case ProofType.Location:
return "Geofence dwell. Being near it does not count as being there.";
case ProofType.Health:
return "Your health platform confirms the workout happened inside the window.";
case ProofType.Witness:
return "Someone else confirms it. The hardest one to talk your way around.";
}
}
/// The capacity refusal. It shows the arithmetic rather than just saying no,
/// because the point is to make overcommitment visible.
void _openCapacitySheet(CapacityVerdict verdict) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
barrierColor: colorPrimaryDark.withValues(alpha: 0.6),
builder: (BuildContext sheetContext) {
return 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: 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("OVER CAPACITY", 9, TextType.Bold,
color: colorStandingWarned, letterSpacing: 1.2),
const SizedBox(height: 10),
text("This day is already full.", 26, TextType.Light,
color: colorPrimaryDark, height: 1.2),
const SizedBox(height: 14),
text(verdict.message, 14, TextType.Regular,
color: colorGrey2, height: 1.55),
const SizedBox(height: 24),
card(
child: Row(
children: [
Expanded(
child: labelled(
"Planned",
formatMinutes(verdict.projectedMinutes),
valueSize: 20,
valueType: TextType.Light,
),
),
Expanded(
child: labelled(
"You do",
formatMinutes(verdict.historicalMinutes),
valueSize: 20,
valueType: TextType.Light,
),
),
Expanded(
child: labelled(
"Cut",
formatMinutes(verdict.excessMinutes),
valueSize: 20,
valueType: TextType.Light,
valueColor: colorStandingGrounded,
),
),
],
),
),
const SizedBox(height: 20),
text(
"Chronic overdue is usually an overcommitment problem wearing a laziness costume. Cutting something now is the cheapest fix available.",
12,
TextType.Regular,
color: colorGrey2,
height: 1.5,
),
const SizedBox(height: 24),
roundedCornerButton(
"Let me cut something",
() => Navigator.pop(sheetContext),
icon: CupertinoIcons.scissors,
),
const SizedBox(height: 8),
Center(
child: textButton(
"Add it anyway",
() {
Navigator.pop(sheetContext);
_model?.saveAnyway(_buildRequest());
},
textSize: 12,
color: colorGrey2,
),
),
],
),
),
);
},
);
}
// ── ConnectNewCommitment ──────────────────────────────────────────────────
@override
void onSaved() {
_model?.showApplicationNotification(
NotificationType.success,
"Committed",
"It is on the record now. The window closes ${formatDateTime(_windowEnd)}.",
true,
true,
() {
Navigator.pop(context, true);
},
);
}
@override
void onCapacityBlocked(CapacityVerdict verdict) {
_openCapacitySheet(verdict);
}
@override
void onMultiplierResolved(double multiplier) {
setState(() {
_multiplier = multiplier;
});
}
@override
void dispose() {
_title.dispose();
_category.dispose();
_estimate.dispose();
super.dispose();
}
}