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:
696
frontend/lib/Grounded/see/home/HomeState.dart
Normal file
696
frontend/lib/Grounded/see/home/HomeState.dart
Normal file
@@ -0,0 +1,696 @@
|
||||
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/ExcuseCluster.dart';
|
||||
import '../../about/internal/application/CommitmentClass.dart';
|
||||
import '../../about/internal/application/CommitmentStatus.dart';
|
||||
import '../../about/internal/application/NavigatorType.dart';
|
||||
import '../../about/internal/application/NotificationType.dart';
|
||||
import '../../about/internal/application/Standing.dart';
|
||||
import '../../about/internal/application/TextType.dart';
|
||||
import '../../about/internal/application/ToneLevel.dart';
|
||||
import '../../about/internal/application/UserDetails.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 '../../utils/StandingEngine.dart';
|
||||
import '../../utils/Thresholds.dart';
|
||||
import '../../utils/ToneEngine.dart';
|
||||
import '../commitment/NewCommitment.dart';
|
||||
import '../excuse/ExcuseReport.dart';
|
||||
import '../goal/Goals.dart';
|
||||
import '../overdue/OverdueQueue.dart';
|
||||
import '../reportcard/ReportCardScreen.dart';
|
||||
import '../settings/Settings.dart';
|
||||
import '../training/Training.dart';
|
||||
import 'ConnectHome.dart';
|
||||
import 'Home.dart';
|
||||
import 'ViewHome.dart';
|
||||
|
||||
class HomeState extends State<Home> implements ConnectHome {
|
||||
ViewHome? _model;
|
||||
|
||||
UserDetails _user = UserDetails(pic: '', name: '');
|
||||
|
||||
List<Commitment> _plan = <Commitment>[];
|
||||
|
||||
List<Commitment> _overdue = <Commitment>[];
|
||||
|
||||
Standing _standing = Standing.Good;
|
||||
|
||||
double _debt = 0;
|
||||
|
||||
ExcuseCluster? _insight;
|
||||
|
||||
bool _distressed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewHome>.reactive(
|
||||
viewModelBuilder: () => ViewHome(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() {
|
||||
_model?.initialise();
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────
|
||||
|
||||
void _onOpenOverdue() async {
|
||||
final result = await GroundedNavigation()
|
||||
.navigateToPageWithData(const OverdueQueue(), context);
|
||||
|
||||
if (result == true) {
|
||||
_model?.loadPlan();
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddCommitment() async {
|
||||
if (!StandingEngine.permitsNewCommitment(_standing)) {
|
||||
_model?.requestNewCommitment(_standing, CommitmentClass.Standard);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await GroundedNavigation()
|
||||
.navigateToPageWithData(const NewCommitment(), context);
|
||||
|
||||
if (result == true) {
|
||||
_model?.loadPlan();
|
||||
}
|
||||
}
|
||||
|
||||
void _onOpenReportCard() {
|
||||
GroundedNavigation().navigateToPage(
|
||||
NavigatorType.justOpen, const ReportCardScreen(), context);
|
||||
}
|
||||
|
||||
void _onOpenGoals() async {
|
||||
final result = await GroundedNavigation()
|
||||
.navigateToPageWithData(const Goals(), context);
|
||||
|
||||
if (result == true) {
|
||||
_model?.loadPlan();
|
||||
}
|
||||
}
|
||||
|
||||
void _onOpenTraining() {
|
||||
GroundedNavigation()
|
||||
.navigateToPage(NavigatorType.justOpen, const Training(), context);
|
||||
}
|
||||
|
||||
void _onOpenSettings() {
|
||||
GroundedNavigation()
|
||||
.navigateToPage(NavigatorType.justOpen, const Settings(), context);
|
||||
}
|
||||
|
||||
void _onOpenExcuses() {
|
||||
GroundedNavigation()
|
||||
.navigateToPage(NavigatorType.justOpen, const ExcuseReport(), context);
|
||||
}
|
||||
|
||||
// ── Views ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _mobileView(BoxConstraints constraints) {
|
||||
// Grounded and Lockdown replace the home screen with the overdue queue —
|
||||
// you do not get to look at your nice plans, only at your mess.
|
||||
final bool queueIsHome =
|
||||
StandingEngine.showsOverdueQueueAsHome(_standing) && !_distressed;
|
||||
|
||||
return Sheet(
|
||||
eyebrow: _user.name.isEmpty ? "Grounded" : _user.name,
|
||||
title: queueIsHome ? "What you owe" : "Today",
|
||||
chrome: _distressed ? colorPrimaryDark : _chromeFor(_standing),
|
||||
banner: _standingBanner(),
|
||||
action: chromeAction(
|
||||
CupertinoIcons.person,
|
||||
_onOpenSettings,
|
||||
dotted: _user.sickMode,
|
||||
dotColor: colorStandingWarned,
|
||||
),
|
||||
child: _distressed
|
||||
? _distressBody()
|
||||
: queueIsHome
|
||||
? _groundedBody()
|
||||
: _planBody(),
|
||||
);
|
||||
}
|
||||
|
||||
/// The chrome carries the standing colour, so the tier is legible before a
|
||||
/// single word is read.
|
||||
Color _chromeFor(Standing standing) {
|
||||
switch (standing) {
|
||||
case Standing.Good:
|
||||
return colorPrimaryDark;
|
||||
case Standing.Warned:
|
||||
return colorPrimaryDark;
|
||||
case Standing.Grounded:
|
||||
return colorStandingGrounded;
|
||||
case Standing.Lockdown:
|
||||
return colorStandingLockdown;
|
||||
}
|
||||
}
|
||||
|
||||
/// The debt strip that sits in the black chrome under the title.
|
||||
Widget _standingBanner() {
|
||||
if (_distressed) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final Color tone = standingColor(_standing);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 7,
|
||||
height: 7,
|
||||
decoration: BoxDecoration(
|
||||
color: _standing == Standing.Good ? tone : colorWhite,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
text(
|
||||
standingLabel(_standing).toUpperCase(),
|
||||
9,
|
||||
TextType.Bold,
|
||||
color: colorWhite.withValues(alpha: 0.75),
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
text(
|
||||
ToneEngine.standingHeadline(_standing, _user.tone),
|
||||
16,
|
||||
TextType.Medium,
|
||||
color: colorWhite,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text("DEBT", 9, TextType.Bold,
|
||||
color: colorWhite.withValues(alpha: 0.45),
|
||||
letterSpacing: 1.0),
|
||||
const SizedBox(height: 4),
|
||||
text(formatDebt(_debt), 30, TextType.Light, color: colorWhite),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The normal day: the plan, with the overdue count kept visible above it so
|
||||
/// it is never out of sight.
|
||||
Widget _planBody() {
|
||||
final int overdueCount = _overdue.length;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (overdueCount > 0) ...[
|
||||
_overdueCallout(overdueCount),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
if (_insight != null) ...[
|
||||
_insightCard(_insight!),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
sectionBreak(
|
||||
"The plan",
|
||||
caption: "${_plan.length} committed",
|
||||
trailing: _plan.isEmpty
|
||||
? null
|
||||
: text(formatMinutes(_plannedMinutes()), 12, TextType.Bold,
|
||||
color: colorGrey2),
|
||||
),
|
||||
if (_plan.isEmpty)
|
||||
emptyState(
|
||||
CupertinoIcons.square_list,
|
||||
"Nothing committed today",
|
||||
"An empty plan is a decision too. Add something you actually intend to do.",
|
||||
)
|
||||
else
|
||||
..._plan.map(_commitmentRow),
|
||||
const SizedBox(height: 28),
|
||||
_quickLinks(),
|
||||
const SizedBox(height: 24),
|
||||
roundedCornerButton(
|
||||
"Commit to something",
|
||||
_onAddCommitment,
|
||||
icon: CupertinoIcons.add,
|
||||
enabled: StandingEngine.permitsNewCommitment(_standing),
|
||||
),
|
||||
if (!StandingEngine.permitsNewCommitment(_standing)) ...[
|
||||
const SizedBox(height: 10),
|
||||
text(
|
||||
ToneEngine.standingBody(_standing, _user.tone),
|
||||
12,
|
||||
TextType.Regular,
|
||||
color: colorGrey2,
|
||||
align: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Grounded: the plan is hidden entirely and only the mess is shown.
|
||||
Widget _groundedBody() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text("YOUR PLANS ARE HIDDEN", 10, TextType.Bold,
|
||||
color: colorGrey2, letterSpacing: 1.2),
|
||||
const SizedBox(height: 10),
|
||||
displayTitle(
|
||||
_standing == Standing.Lockdown
|
||||
? "One at a time."
|
||||
: "Clear this first.",
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
text(
|
||||
ToneEngine.standingBody(_standing, _user.tone),
|
||||
14,
|
||||
TextType.Regular,
|
||||
color: colorGrey2,
|
||||
height: 1.55,
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
card(
|
||||
background: standingBackground(_standing),
|
||||
borderColor: standingColor(_standing).withValues(alpha: 0.20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: labelled(
|
||||
"Open overdue",
|
||||
"${_overdue.length}",
|
||||
valueSize: 26,
|
||||
valueType: TextType.Light,
|
||||
valueColor: standingColor(_standing),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: labelled(
|
||||
"Debt to clear",
|
||||
formatDebt(
|
||||
StandingEngine.debtToNextTierDown(_debt, _standing)),
|
||||
valueSize: 26,
|
||||
valueType: TextType.Light,
|
||||
valueColor: standingColor(_standing),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
sectionBreak("Outstanding", caption: "${_overdue.length} items"),
|
||||
if (_overdue.isEmpty)
|
||||
emptyState(
|
||||
CupertinoIcons.checkmark_seal,
|
||||
"The queue is empty",
|
||||
"Your standing will recover as the debt decays.",
|
||||
)
|
||||
else
|
||||
..._overdue.take(_standing == Standing.Lockdown ? 1 : _overdue.length)
|
||||
.map(_commitmentRow),
|
||||
const SizedBox(height: 24),
|
||||
roundedCornerButton(
|
||||
_standing == Standing.Lockdown ? "Deal with this one" : "Open the queue",
|
||||
_onOpenOverdue,
|
||||
background: standingColor(_standing),
|
||||
icon: CupertinoIcons.arrow_right,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Distress: the strict persona drops entirely. This is the difference
|
||||
/// between a product people keep and one they resent.
|
||||
Widget _distressBody() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text("A NOTE", 10, TextType.Bold, color: colorGrey2, letterSpacing: 1.2),
|
||||
const SizedBox(height: 10),
|
||||
displayTitle(ToneEngine.distressHeadline()),
|
||||
const SizedBox(height: 14),
|
||||
text(
|
||||
ToneEngine.distressBody(),
|
||||
15,
|
||||
TextType.Regular,
|
||||
color: colorGrey2,
|
||||
height: 1.6,
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
card(
|
||||
background: colorStandingGoodBg,
|
||||
borderColor: colorPositive.withValues(alpha: 0.20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text("PAUSED", 9, TextType.Bold,
|
||||
color: colorPositive, letterSpacing: 1.2),
|
||||
const SizedBox(height: 8),
|
||||
text("Debt is not accruing right now.", 16, TextType.Medium,
|
||||
color: colorPrimaryDark),
|
||||
const SizedBox(height: 6),
|
||||
text(
|
||||
"Nothing you miss this week is counting against you.",
|
||||
13,
|
||||
TextType.Regular,
|
||||
color: colorGrey2,
|
||||
height: 1.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
sectionBreak("Three things", caption: "that actually matter"),
|
||||
..._plan
|
||||
.where((item) =>
|
||||
item.commitmentClass == CommitmentClass.NonNegotiable)
|
||||
.take(3)
|
||||
.map(_commitmentRow),
|
||||
const SizedBox(height: 24),
|
||||
outlinedActionButton("Open settings", _onOpenSettings,
|
||||
icon: CupertinoIcons.slider_horizontal_3),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _overdueCallout(int count) {
|
||||
return card(
|
||||
background: colorStandingGroundedBg,
|
||||
borderColor: colorStandingGrounded.withValues(alpha: 0.20),
|
||||
onTap: _onOpenOverdue,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: colorStandingGrounded,
|
||||
borderRadius: BorderRadius.circular(13),
|
||||
),
|
||||
child: text("$count", 17, TextType.Bold, color: colorWhite),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text("OVERDUE", 9, TextType.Bold,
|
||||
color: colorStandingGrounded, letterSpacing: 1.2),
|
||||
const SizedBox(height: 5),
|
||||
text(
|
||||
count >= Thresholds.maxOpenOverdue
|
||||
? "You are at the cap. Nothing new until this drops."
|
||||
: "$count item${count == 1 ? "" : "s"} past the window.",
|
||||
14,
|
||||
TextType.Medium,
|
||||
color: colorPrimaryDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(CupertinoIcons.chevron_right,
|
||||
size: 15, color: colorStandingGrounded),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The excuse confrontation. One pattern, stated plainly, with the
|
||||
/// suggestion attached.
|
||||
Widget _insightCard(ExcuseCluster cluster) {
|
||||
return card(
|
||||
background: colorPrimaryDark,
|
||||
borderColor: colorPrimaryDark,
|
||||
onTap: _onOpenExcuses,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
text("PATTERN", 9, TextType.Bold,
|
||||
color: colorWhite.withValues(alpha: 0.45),
|
||||
letterSpacing: 1.2),
|
||||
text("${cluster.occurrences}×", 11, TextType.Bold,
|
||||
color: colorWhite.withValues(alpha: 0.45)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
text(cluster.insight, 15, TextType.Regular,
|
||||
color: colorWhite, height: 1.55),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _commitmentRow(Commitment item) {
|
||||
final bool late = item.windowClosed &&
|
||||
item.status != CommitmentStatus.Completed &&
|
||||
item.status != CommitmentStatus.LateCompleted;
|
||||
|
||||
final Color accent = classColor(item.commitmentClass);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: card(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
|
||||
onTap: _onOpenOverdue,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 3,
|
||||
height: 42,
|
||||
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(item.title, 15, TextType.Medium,
|
||||
color: colorPrimaryDark,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
text(formatWindow(item), 11, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
const SizedBox(width: 10),
|
||||
Container(width: 3, height: 3, decoration: BoxDecoration(
|
||||
color: colorGrey, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 10),
|
||||
text(formatMinutes(item.estMinutes), 11,
|
||||
TextType.Regular, color: colorGrey2),
|
||||
],
|
||||
),
|
||||
if (late) ...[
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
pill(
|
||||
overdueLabel(item),
|
||||
colorStandingGrounded,
|
||||
colorStandingGroundedBg,
|
||||
textSize: 9,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
pill(
|
||||
"−${formatDebt(DebtEngine.commitmentDebt(item))}",
|
||||
colorGrey2,
|
||||
colorMuted,
|
||||
textSize: 9,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
pill(
|
||||
classLabel(item.commitmentClass),
|
||||
accent,
|
||||
classBackground(item.commitmentClass),
|
||||
textSize: 9,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _quickLinks() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _quickLink(
|
||||
CupertinoIcons.flag_fill,
|
||||
"Goals",
|
||||
_onOpenGoals,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _quickLink(
|
||||
CupertinoIcons.chart_bar_alt_fill,
|
||||
"Report",
|
||||
_onOpenReportCard,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _quickLink(
|
||||
CupertinoIcons.flame_fill,
|
||||
"Training",
|
||||
_onOpenTraining,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _quickLink(IconData icon, String label, VoidCallback onTap) {
|
||||
return card(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 16),
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 17, color: colorPrimaryDark),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: text(label, 13, TextType.Medium,
|
||||
color: colorPrimaryDark, maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _plannedMinutes() {
|
||||
double total = 0;
|
||||
for (Commitment item in _plan) {
|
||||
total = total + item.estMinutes;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// ── ConnectHome ───────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onUserLoaded(UserDetails details) {
|
||||
setState(() {
|
||||
_user = details;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onPlanLoaded(List<Commitment> plan) {
|
||||
setState(() {
|
||||
_plan = plan;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onOverdueLoaded(List<Commitment> overdue) {
|
||||
setState(() {
|
||||
_overdue = overdue;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onStandingResolved(Standing standing, double debtScore) {
|
||||
setState(() {
|
||||
_standing = standing;
|
||||
_debt = debtScore;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onExcuseInsight(ExcuseCluster? cluster) {
|
||||
setState(() {
|
||||
_insight = cluster;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onDistressDetected() {
|
||||
setState(() {
|
||||
_distressed = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onCreationBlocked(String reason) {
|
||||
_model?.showApplicationNotification(
|
||||
NotificationType.warning,
|
||||
"Not right now",
|
||||
reason,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user