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 '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() { _openGoalSheet(); } /// 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 formKey = GlobalKey(); 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( 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), ), ], ), ), ), ), ); }, ); }, ); } 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), ], ), ); } Widget _goalCard(Goal goal) { final Color accent = goal.slipping ? colorStandingGrounded : colorPrimaryDark; return Container( margin: const EdgeInsets.only(bottom: 12), child: card( onTap: () => _onOpenGoal(goal), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ text(goal.title, 19, TextType.Light, color: colorPrimaryDark, maxLines: 2, overflow: TextOverflow.ellipsis), if (goal.description.isNotEmpty) ...[ const SizedBox(height: 6), text(goal.description, 12, TextType.Regular, color: colorGrey2, maxLines: 2, overflow: TextOverflow.ellipsis), ], ], ), ), const SizedBox(width: 10), if (goal.overdueTasks > 0) pill("${goal.overdueTasks} late", colorStandingGrounded, colorStandingGroundedBg, textSize: 9), ], ), const SizedBox(height: 18), meter(goal.progress, fill: accent), const SizedBox(height: 12), Row( children: [ Expanded( child: labelled( "Done", "${goal.completedTasks} of ${goal.totalTasks}", valueSize: 13, ), ), Expanded( child: labelled( "Remaining", "${goal.remainingTasks}", valueSize: 13, ), ), Expanded( child: labelled( "Debt", formatDebt(goal.debtContribution), valueSize: 13, valueColor: goal.debtContribution > 0 ? colorStandingGrounded : colorPrimaryDark, ), ), ], ), ], ), ), ); } @override void onGoalsLoaded(List goals) { setState(() { _goals = goals; }); } @override void onGoalSaved() { _model?.loadGoals(); } }