import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import '../../about/internal/application/NavigatorType.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/input/InputFields.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; import '../../utils/Thresholds.dart'; import '../../utils/Validators.dart'; import '../login/Login.dart'; import 'ConnectSettings.dart'; import 'Settings.dart'; import 'ViewSettings.dart'; class SettingsState extends State implements ConnectSettings { ViewSettings? _model; UserDetails _user = UserDetails(pic: '', name: ''); int _amnestyRemaining = 0; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewSettings(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 _onBack() { Navigator.pop(context); } void _onToneSelected(ToneLevel tone) { _model?.changeTone(tone); } void _onToggleSickMode() { if (_user.sickMode) { _model?.setSickMode(false, ""); return; } _openSickModeSheet(); } void _onSignOut() { _model?.signOut(); } void _openSickModeSheet() { final TextEditingController reason = TextEditingController(); final GlobalKey formKey = GlobalKey(); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, barrierColor: colorPrimaryDark.withValues(alpha: 0.6), builder: (BuildContext sheetContext) { 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("PAUSE", 9, TextType.Bold, color: colorGrey2, letterSpacing: 1.2), const SizedBox(height: 10), text("Sick or travelling.", 26, TextType.Light, color: colorPrimaryDark, height: 1.2), const SizedBox(height: 14), text( "Debt stops accruing entirely while this is on. It is logged in your history, which is the only reason it stays honest.", 14, TextType.Regular, color: colorGrey2, height: 1.55, ), const SizedBox(height: 24), inputField( "Reason", reason, hint: "What is going on?", validator: Validators.excuse, maxLines: 3, ), const SizedBox(height: 24), roundedCornerButton( "Pause everything", () { if (formKey.currentState?.validate() != true) { return; } Navigator.pop(sheetContext); _model?.setSickMode(true, reason.text.trim()); }, icon: CupertinoIcons.pause_fill, ), const SizedBox(height: 8), Center( child: textButton( "Cancel", () => Navigator.pop(sheetContext), textSize: 13, ), ), ], ), ), ), ), ); }, ); } // ── Views ───────────────────────────────────────────────────────────────── Widget _mobileView(BoxConstraints constraints) { return Sheet( eyebrow: _user.name.isEmpty ? "Account" : _user.name, title: "Settings", onBack: _onBack, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ sectionBreak("Tone", caption: "how it speaks to you"), segmentedSelector( options: ToneLevel.values, selected: _user.tone, label: toneLabel, onSelected: _onToneSelected, ), const SizedBox(height: 12), text( _toneDescription(_user.tone), 12, TextType.Regular, color: colorGrey2, height: 1.5, ), const SizedBox(height: 10), text( "Whatever you pick, nothing here will attack you as a person. It criticises what you did, never who you are.", 11, TextType.Regular, color: colorGrey, height: 1.5, ), const SizedBox(height: 28), sectionBreak("Amnesty", caption: "rationed on purpose"), card( child: Row( children: [ Expanded( child: labelled( "Remaining this month", "$_amnestyRemaining of ${Thresholds.amnestyTokensPerMonth}", valueSize: 20, valueType: TextType.Light, ), ), Container( width: 46, height: 46, alignment: Alignment.center, decoration: BoxDecoration( color: _amnestyRemaining > 0 ? colorStandingGoodBg : colorMuted, borderRadius: BorderRadius.circular(14), ), child: Icon( CupertinoIcons.checkmark_shield_fill, size: 20, color: _amnestyRemaining > 0 ? colorPositive : colorGrey, ), ), ], ), ), const SizedBox(height: 10), text( "Tokens wipe an item's debt, no questions asked. They exist so one bad flu does not undo three months.", 12, TextType.Regular, color: colorGrey2, height: 1.5, ), const SizedBox(height: 28), sectionBreak("Pause", caption: "sick or travel"), card( background: _user.sickMode ? colorStandingGoodBg : colorCard, borderColor: _user.sickMode ? colorPositive.withValues(alpha: 0.20) : colorBorder, onTap: _onToggleSickMode, child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ text( _user.sickMode ? "Paused" : "Running", 16, TextType.Medium, color: colorPrimaryDark, ), const SizedBox(height: 6), text( _user.sickMode ? "Debt is not accruing. Tap to resume." : "Debt is accruing normally. Tap to pause.", 12, TextType.Regular, color: colorGrey2, ), ], ), ), Icon( _user.sickMode ? CupertinoIcons.pause_circle_fill : CupertinoIcons.play_circle, size: 26, color: _user.sickMode ? colorPositive : colorGrey2, ), ], ), ), const SizedBox(height: 36), outlinedActionButton( "Sign out", _onSignOut, foreground: colorStandingLockdown, icon: CupertinoIcons.square_arrow_right, ), const SizedBox(height: 20), Center( child: text("Grounded 1.0.0", 11, TextType.Regular, color: colorGrey), ), ], ), ); } String _toneDescription(ToneLevel tone) { switch (tone) { case ToneLevel.Firm: return "Direct and unsentimental. States the facts and leaves them there."; case ToneLevel.Strict: return "Holds you to what you said. Disappointment rather than anger, because it works better."; case ToneLevel.DrillSergeant: return "Blunt and relentless about the behaviour. Still never about you."; } } // ── ConnectSettings ─────────────────────────────────────────────────────── @override void onUserLoaded(UserDetails details, int amnestyRemaining) { setState(() { _user = details; _amnestyRemaining = amnestyRemaining; }); } @override void onToneChanged(UserDetails details) { setState(() { _user = details; }); } @override void onSickModeChanged(UserDetails details) { setState(() { _user = details; }); } @override void onSignedOut() { GroundedNavigation() .navigateToPage(NavigatorType.makeNewMain, const Login(), context); } }