import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import '../../about/external/data/RoutineChain.dart'; import '../../about/external/data/RoutineStep.dart'; import '../../about/internal/application/CommitmentClass.dart'; import '../../about/internal/application/RoutineKind.dart'; import '../../about/internal/application/TextType.dart'; import '../../configs/Navigator.dart'; import '../../designs/Component.dart'; import '../../designs/ProgressRing.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 'ConnectRoutines.dart'; import 'RoutineBuilder.dart'; import 'RoutineRunner.dart'; import 'Routines.dart'; import 'ViewRoutines.dart'; /// The routine library: what is due today up front, everything else below, /// filtered by kind. class RoutinesState extends State implements ConnectRoutines { ViewRoutines? _model; List _routines = []; /// Null shows every kind. RoutineKind? _kind; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewRoutines(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?.loadRoutines(); } // ── Derived ─────────────────────────────────────────────────────────────── List get _filtered { if (_kind == null) { return _routines; } return _routines.where((item) => item.kind == _kind).toList(); } List get _today { final DateTime now = DateTime.now(); return _filtered.where((item) => item.scheduledOn(now)).toList(); } List get _rest { final DateTime now = DateTime.now(); return _filtered.where((item) => !item.scheduledOn(now)).toList(); } /// Today's routines that are not finished, soonest first — the featured /// rail. Falls back to the whole library when nothing is due. List get _upNext { final List open = _today.where((item) => !item.complete).toList(); final List source = open.isEmpty ? _filtered : open; return source.take(5).toList(); } int _countFor(RoutineKind? kind) { if (kind == null) { return _routines.length; } return _routines.where((item) => item.kind == kind).length; } // ── Handlers ────────────────────────────────────────────────────────────── void _onBack() { Navigator.pop(context); } void _onSelectKind(RoutineKind? kind) { setState(() { _kind = _kind == kind ? null : kind; }); } void _onBuild() async { final result = await GroundedNavigation() .navigateToPageWithData(RoutineBuilder(kind: _kind), context); if (result == true) { _model?.loadRoutines(); } } void _onEdit(RoutineChain routine) async { final result = await GroundedNavigation() .navigateToPageWithData(RoutineBuilder(routine: routine), context); if (result == true) { _model?.loadRoutines(); } } void _onStart(RoutineChain routine) async { if (routine.steps.isEmpty) { _onEdit(routine); return; } final result = await GroundedNavigation() .navigateToPageWithData(RoutineRunner(routine: routine.copy()), context); if (result == true) { _model?.loadRoutines(); } } void _onPreview(RoutineChain routine) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, barrierColor: colorBlack.withValues(alpha: 0.55), builder: (BuildContext sheetContext) { return _previewSheet(sheetContext, routine); }, ); } // ── Views ───────────────────────────────────────────────────────────────── Widget _mobileView(BoxConstraints constraints) { return Sheet( eyebrow: "${_routines.length} routine${_routines.length == 1 ? "" : "s"}", title: "Routines", onBack: _onBack, action: chromeAction(CupertinoIcons.add, _onBuild), banner: _routines.isEmpty ? null : _todayBanner(), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ text( "${weekdayName(DateTime.now().weekday).toUpperCase()} · ${_today.length} DUE", 11, TextType.Bold, color: colorGrey2, letterSpacing: 1.2, ), const SizedBox(height: 8), displayTitle(_headline()), const SizedBox(height: 24), _kindRail(), const SizedBox(height: 28), if (_filtered.isEmpty) emptyState( _kind == null ? CupertinoIcons.square_stack_3d_up : kindIcon(_kind!), _kind == null ? "No routines yet" : "No ${kindLabel(_kind!).toLowerCase()} routines", "A routine is a chain: every step, in order. Break it and it is logged as partial, not quietly forgotten.", ) else ...[ sectionBreak( "Up next", caption: _today.isEmpty ? "from your library" : "today", ), _featuredRail(), const SizedBox(height: 28), if (_today.isNotEmpty) ...[ sectionBreak( "Scheduled today", caption: "${_today.where((item) => item.complete).length} of ${_today.length} done", ), ..._today.map(_routineRow), const SizedBox(height: 20), ], if (_rest.isNotEmpty) ...[ sectionBreak("Library", caption: "${_rest.length} more"), ..._rest.map(_routineRow), ], ], const SizedBox(height: 24), roundedCornerButton( "Build a routine", _onBuild, icon: CupertinoIcons.add, ), ], ), ); } String _headline() { if (_routines.isEmpty) { return "Build the things you do every time."; } final int open = _today.where((item) => !item.complete).length; if (_today.isEmpty) { return "Nothing scheduled. Run one anyway."; } if (open == 0) { return "Every chain held today."; } return "Here's what to hold today."; } /// The strip in the black chrome: today's load at a glance. Widget _todayBanner() { final DateTime now = DateTime.now(); final List due = _routines.where((item) => item.scheduledOn(now)).toList(); int seconds = 0; for (RoutineChain item in due) { seconds = seconds + item.totalSeconds; } final int nowMinute = now.hour * 60 + now.minute; final List ahead = due .where((item) => !item.complete && item.startMinute >= nowMinute) .toList() ..sort((a, b) => a.startMinute.compareTo(b.startMinute)); final Color label = colorWhite.withValues(alpha: 0.45); return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: BoxDecoration( color: colorWhite.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Expanded( child: labelled("Due today", "${due.length}", valueSize: 22, valueType: TextType.Light, valueColor: colorWhite, labelColor: label), ), Expanded( child: labelled("Planned", formatMinutes(seconds / 60), valueSize: 22, valueType: TextType.Light, valueColor: colorWhite, labelColor: label), ), Expanded( child: labelled( "Next", ahead.isEmpty ? "—" : formatStartMinute(ahead.first.startMinute), valueSize: 22, valueType: TextType.Light, valueColor: colorWhite, labelColor: label), ), ], ), ); } /// Round category tiles — tap to filter, tap again to clear. Widget _kindRail() { final List options = [null, ...RoutineKind.values]; return SizedBox( height: 96, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: options.length, separatorBuilder: (context, index) => const SizedBox(width: 14), itemBuilder: (context, index) => _kindTile(options[index]), ), ); } Widget _kindTile(RoutineKind? kind) { final bool selected = _kind == kind; final Color tone = kind == null ? colorPrimaryDark : kindColor(kind); return GestureDetector( onTap: () => _onSelectKind(kind), behavior: HitTestBehavior.opaque, child: SizedBox( width: 70, child: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeOut, width: 60, height: 60, alignment: Alignment.center, decoration: BoxDecoration( shape: BoxShape.circle, gradient: selected ? LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ tone, kind == null ? colorPrimaryDark2 : kindDeep(kind), ], ) : null, color: selected ? null : colorWhite, border: Border.all( color: selected ? tone : colorBorder, width: selected ? 2 : 1, ), ), child: Icon( kind == null ? CupertinoIcons.square_grid_2x2_fill : kindIcon(kind), size: 22, color: selected ? colorWhite : tone, ), ), const SizedBox(height: 8), text( kind == null ? "All" : kindLabel(kind), 11, selected ? TextType.Bold : TextType.Regular, color: selected ? colorPrimaryDark : colorGrey2, maxLines: 1, ), const SizedBox(height: 2), text("${_countFor(kind)}", 9, TextType.Bold, color: colorGrey, letterSpacing: 0.6), ], ), ), ); } Widget _featuredRail() { final List items = _upNext; return SizedBox( height: 214, child: ListView.separated( scrollDirection: Axis.horizontal, clipBehavior: Clip.none, itemCount: items.length, separatorBuilder: (context, index) => const SizedBox(width: 12), itemBuilder: (context, index) => _featuredCard(items[index]), ), ); } /// The large gradient card. The kind's icon sits oversized in the corner as /// the card's picture — routines have no stock photography. Widget _featuredCard(RoutineChain routine) { final Color tone = kindColor(routine.kind); final Color label = colorWhite.withValues(alpha: 0.55); return GestureDetector( onTap: () => _onPreview(routine), child: Container( width: 248, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [tone, kindDeep(routine.kind)], ), boxShadow: const [ BoxShadow( color: Color(0x08000000), blurRadius: 18, offset: Offset(0, 4), ), ], ), clipBehavior: Clip.antiAlias, child: Stack( children: [ Positioned( right: -26, bottom: 34, child: Icon( kindIcon(routine.kind), size: 150, color: colorWhite.withValues(alpha: 0.08), ), ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ pill(kindLabel(routine.kind), colorWhite, colorWhite.withValues(alpha: 0.14), textSize: 9), if (routine.commitmentClass == CommitmentClass.NonNegotiable) ...[ const SizedBox(width: 6), pill("Must hold", colorWhite, colorWhite.withValues(alpha: 0.14), textSize: 9), ], const Spacer(), Container( width: 34, height: 34, alignment: Alignment.center, decoration: BoxDecoration( color: colorWhite, shape: BoxShape.circle, ), child: Icon(CupertinoIcons.play_fill, size: 14, color: kindDeep(routine.kind)), ), ], ), const Spacer(), if (routine.startMinute >= 0) ...[ text( "${formatStartMinute(routine.startMinute)} · ${formatDays(routine.days)}" .toUpperCase(), 9, TextType.Bold, color: label, letterSpacing: 1.0, ), const SizedBox(height: 6), ], text(routine.title, 21, TextType.Light, color: colorWhite, maxLines: 2, overflow: TextOverflow.ellipsis, height: 1.15), const SizedBox(height: 14), Container( height: 1, color: colorWhite.withValues(alpha: 0.12), ), const SizedBox(height: 12), Row( children: [ Expanded( child: labelled( "Minutes", "${(routine.totalSeconds / 60).round()}", valueSize: 17, valueColor: colorWhite, labelColor: label, ), ), Expanded( child: labelled( "Steps", "${routine.steps.length}", valueSize: 17, valueColor: colorWhite, labelColor: label, ), ), Expanded( child: labelled( routine.kind == RoutineKind.Workout ? "Sets" : "Starts", routine.kind == RoutineKind.Workout ? "${routine.totalSets}" : routine.startMinute < 0 ? "Any" : formatStartMinute(routine.startMinute), valueSize: 17, valueColor: colorWhite, labelColor: label, ), ), ], ), ], ), ), ], ), ), ); } Widget _routineRow(RoutineChain routine) { final Color tone = kindColor(routine.kind); final bool weighted = routine.commitmentClass != CommitmentClass.Standard; final List eyebrow = [ if (weighted) classLabel(routine.commitmentClass), if (routine.startMinute >= 0) formatStartMinute(routine.startMinute), formatDays(routine.days), ]; return Container( margin: const EdgeInsets.only(bottom: 10), child: card( padding: const EdgeInsets.fromLTRB(10, 10, 14, 10), onTap: () => _onPreview(routine), child: Row( children: [ Container( width: 58, height: 58, decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [kindBackground(routine.kind), colorWhite], ), border: Border.all(color: tone.withValues(alpha: 0.15)), ), alignment: Alignment.center, child: Icon(kindIcon(routine.kind), size: 24, color: tone), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ text( eyebrow.join(" · ").toUpperCase(), 9, TextType.Bold, color: weighted ? classColor(routine.commitmentClass) : colorGrey2, letterSpacing: 0.9, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 5), text(routine.title, 15, TextType.Medium, color: colorPrimaryDark, maxLines: 1, overflow: TextOverflow.ellipsis), const SizedBox(height: 6), Row( children: [ _meta(CupertinoIcons.clock, formatMinutes(routine.totalSeconds / 60)), const SizedBox(width: 12), _countMeta(routine), ], ), ], ), ), const SizedBox(width: 10), routine.complete ? Container( width: 44, height: 44, alignment: Alignment.center, decoration: BoxDecoration( color: tone, shape: BoxShape.circle, ), child: Icon(Icons.check_rounded, size: 22, color: colorWhite), ) : ProgressRing( fraction: routine.partialCompletion, fill: tone, ), ], ), ), ); } Widget _countMeta(RoutineChain routine) { switch (routine.kind) { case RoutineKind.Workout: return _meta(CupertinoIcons.repeat, "${routine.totalSets} sets"); case RoutineKind.Meditation: return _meta(CupertinoIcons.circle_grid_hex, "${routine.steps.length} steps"); case RoutineKind.MustDo: return _meta(CupertinoIcons.checkmark_square, "${routine.steps.length} tasks"); } } Widget _meta(IconData icon, String value) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 12, color: colorGrey2), const SizedBox(width: 4), text(value, 11, TextType.Regular, color: colorGrey2), ], ); } /// The preview: the whole chain laid out before you commit to starting it. Widget _previewSheet(BuildContext sheetContext, RoutineChain routine) { final Color tone = kindColor(routine.kind); return Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(sheetContext).size.height * 0.86, ), decoration: const BoxDecoration( color: colorSheetBackground, borderRadius: BorderRadius.only( topLeft: Radius.circular(28), topRight: Radius.circular(28), ), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(top: 10), decoration: BoxDecoration( color: colorGrey.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(999), ), ), ), Flexible( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(24, 20, 24, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ pill(kindLabel(routine.kind), tone, kindBackground(routine.kind), icon: kindIcon(routine.kind), textSize: 9), const SizedBox(width: 6), pill(classLabel(routine.commitmentClass), classColor(routine.commitmentClass), classBackground(routine.commitmentClass), textSize: 9), ], ), const SizedBox(height: 14), displayTitle(routine.title, size: 30), if (routine.note.isNotEmpty) ...[ const SizedBox(height: 8), text(routine.note, 13, TextType.Regular, color: colorGrey2, height: 1.5), ], const SizedBox(height: 20), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: statTile("Minutes", "${(routine.totalSeconds / 60).round()}")), Expanded( child: statTile("Steps", "${routine.steps.length}")), Expanded( child: statTile( "When", routine.startMinute < 0 ? "Any" : formatStartMinute(routine.startMinute), caption: formatDays(routine.days), ), ), ], ), const SizedBox(height: 24), sectionBreak("The chain", caption: routine.kind == RoutineKind.Workout ? "${routine.totalSets} sets, in order" : "${routine.steps.length} steps, in order"), ...routine.steps .asMap() .entries .map((entry) => _previewStep(entry.key, entry.value, tone)), ], ), ), ), SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(24, 8, 24, 16), child: Row( children: [ Expanded( child: outlinedActionButton("Edit", () { Navigator.pop(sheetContext); _onEdit(routine); }, icon: CupertinoIcons.pencil), ), const SizedBox(width: 10), Expanded( flex: 2, child: roundedCornerButton("Start", () { Navigator.pop(sheetContext); _onStart(routine); }, icon: CupertinoIcons.play_fill, enabled: routine.steps.isNotEmpty), ), ], ), ), ), ], ), ); } Widget _previewStep(int index, RoutineStep step, Color tone) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 28, height: 28, alignment: Alignment.center, decoration: BoxDecoration( color: tone.withValues(alpha: 0.10), shape: BoxShape.circle, ), child: text("${index + 1}", 11, TextType.Bold, color: tone), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 4), text(step.title, 14, TextType.Medium, color: colorPrimaryDark), const SizedBox(height: 3), text(stepSummary(step), 11, TextType.Regular, color: colorGrey2), ], ), ), Padding( padding: const EdgeInsets.only(top: 5), child: Icon(stepModeIcon(step.mode), size: 15, color: colorGrey), ), ], ), ); } // ── ConnectRoutines ─────────────────────────────────────────────────────── @override void onRoutinesLoaded(List routines) { setState(() { _routines = routines; }); } }