Files
GroundedHelper/frontend/lib/Grounded/see/settings/SettingsState.dart
alvocool d3431ca5de Switch to Inter, round the corners softly and make the nav switch tabs
Inter replaces General Sans (Inter Display for titles and big numbers).
Corners come back from before the squaring commit, scaled to a soft set
(16 cards, 14 buttons, 12 rows, 8 chips) and applied to the screens built
since. The nav is now a cluster of round frosted buttons that switches
Today, Routines, Goals, Groups and Progress in place inside Home: no
pushed pages, no back arrows, and Android back returns to Today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-09 07:08:46 +03:00

379 lines
13 KiB
Dart

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 '../progress/TargetSetup.dart';
import 'ConnectSettings.dart';
import 'Settings.dart';
import 'ViewSettings.dart';
class SettingsState extends State<Settings> implements ConnectSettings {
ViewSettings? _model;
UserDetails _user = UserDetails(pic: '', name: '');
int _amnestyRemaining = 0;
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ViewSettings>.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 _onOpenTargets() {
GroundedNavigation()
.navigateToPageWithData(const TargetSetup(), 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<FormState> formKey = GlobalKey<FormState>();
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(16),
topRight: Radius.circular(16),
),
),
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(14),
),
),
),
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<ToneLevel>(
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(12),
),
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("Targets", caption: "what a good week is"),
card(
onTap: _onOpenTargets,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
text("Targets & reminders", 16, TextType.Medium,
color: colorPrimaryDark),
const SizedBox(height: 6),
text(
"Routines and active time per week, and a heads-up before each routine.",
12,
TextType.Regular,
color: colorGrey2,
),
],
),
),
Icon(CupertinoIcons.chevron_right,
size: 18, color: colorGrey2),
],
),
),
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);
}
}