import 'package:flutter/cupertino.dart'; import 'package:stacked/stacked.dart'; import '../../about/external/data/Goal.dart'; import '../../about/internal/application/TextType.dart'; import '../../configs/Navigator.dart'; import '../../designs/Component.dart'; import '../../designs/GlassSurface.dart'; import '../../designs/Responsive.dart'; import '../../designs/Shell.dart'; import '../../designs/buttons/Buttons.dart'; import '../../designs/scene/SceneArt.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; import '../../utils/CommonUtils.dart'; import 'ConnectGoals.dart'; import 'GoalDetail.dart'; import 'GoalEditor.dart'; import 'Goals.dart'; import 'ViewGoals.dart'; class GoalsState extends State implements ConnectGoals { ViewGoals? _model; List _goals = []; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewGoals(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?.loadGoals(); } void _onBack() { Navigator.pop(context); } void _onOpenGoal(Goal goal) async { final result = await GroundedNavigation() .navigateToPageWithData(GoalDetail(goal: goal), context); if (result == true) { _model?.loadGoals(); } } void _onNewGoal() async { final result = await GroundedNavigation() .navigateToPageWithData(const GoalEditor(), context); if (result is Goal) { _model?.loadGoals(); } } Widget _mobileView(BoxConstraints constraints) { return Sheet( eyebrow: "Grounded", title: "Goals", onBack: _onBack, action: chromeAction(CupertinoIcons.add, _onNewGoal), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ displayTitle("What you are\nworking toward."), const SizedBox(height: 14), text( "A goal holds the tasks that get you there. The goal never carries debt โ€” the tasks inside it do.", 14, TextType.Regular, color: colorGrey2, height: 1.55, ), const SizedBox(height: 28), if (_goals.isEmpty) emptyState( CupertinoIcons.flag, "No goals yet", "Create one โ€” Workout, say โ€” then put the actual sessions inside it.", ) else ..._goals.map(_goalCard), const SizedBox(height: 24), roundedCornerButton("New goal", _onNewGoal, icon: CupertinoIcons.add), ], ), ); } /// The goal as a painted card in its own colour: the ambition up top, the /// progress and the cost along the bottom. Widget _goalCard(Goal goal) { final Color accent = goalColor(goal.colourHex); final Color soft = colorWhite.withValues(alpha: 0.78); final bool overdue = goal.targetDate != null && goal.targetDate!.isBefore(DateTime.now()); return GestureDetector( onTap: () => _onOpenGoal(goal), child: Container( height: 214, margin: const EdgeInsets.only(bottom: 14), decoration: BoxDecoration( borderRadius: BorderRadius.circular(26), boxShadow: const [ BoxShadow( color: Color(0x262A3A30), blurRadius: 24, offset: Offset(0, 10), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(26), child: Stack( fit: StackFit.expand, children: [ SceneArt( palette: goalScene(accent), seed: sceneSeed(goal.title), figure: CupertinoIcons.flag_fill, figureScale: 0.4, figureAlignment: const Alignment(0.75, -0.35), shade: true, ), Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ GlassSurface( onDark: true, radius: BorderRadius.circular(99), padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5), child: text( [ if (goal.category.isNotEmpty) goal.category, goal.targetDate == null ? "No deadline" : targetLabel(goal.targetDate), ].join(" ยท "), 10, TextType.Medium, color: overdue ? colorTinted : colorWhite, ), ), const Spacer(), if (goal.overdueTasks > 0) pill("${goal.overdueTasks} late", colorWhite, colorStandingGrounded, textSize: 9), ], ), const Spacer(), text(goal.title, 21, TextType.Medium, color: colorWhite, maxLines: 1, overflow: TextOverflow.ellipsis), if (goal.description.isNotEmpty) ...[ const SizedBox(height: 3), text(goal.description, 12, TextType.Regular, color: soft, maxLines: 1, overflow: TextOverflow.ellipsis), ], const SizedBox(height: 12), meter(goal.progress, fill: colorLime, track: colorWhite.withValues(alpha: 0.25), height: 5), const SizedBox(height: 10), Row( children: [ Expanded( child: _cardStat( "${goal.completedTasks}/${goal.totalTasks}", "Done", soft), ), Expanded( child: _cardStat( "${goal.remainingTasks}", "Remaining", soft), ), Expanded( child: _cardStat( formatDebt(goal.debtContribution), "Debt", soft, alert: goal.debtContribution > 0), ), ], ), ], ), ), ], ), ), ), ); } Widget _cardStat(String value, String caption, Color tone, {bool alert = false}) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ text(value, 18, TextType.Light, color: alert ? colorTinted : colorWhite), const SizedBox(width: 5), Padding( padding: const EdgeInsets.only(bottom: 2), child: text(caption, 10, TextType.Regular, color: tone), ), ], ); } @override void onGoalsLoaded(List goals) { setState(() { _goals = goals; }); } }