Files
GroundedHelper/frontend/lib/Grounded/see/progress/TargetSetupState.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

307 lines
9.3 KiB
Dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:stacked/stacked.dart';
import '../../about/internal/application/ProgressTargets.dart';
import '../../about/internal/application/TextType.dart';
import '../../designs/Component.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 'ConnectTargetSetup.dart';
import 'TargetSetup.dart';
import 'ViewTargetSetup.dart';
class TargetSetupState extends State<TargetSetup>
implements ConnectTargetSetup {
ViewTargetSetup? _model;
ProgressTargets _targets = ProgressTargets();
int _scheduled = 0;
static const int _routineFloor = 1;
static const int _routineCeiling = 28;
static const List<int> _minuteOptions = <int>[60, 90, 150, 210, 300];
static const List<int> _leadOptions = <int>[0, 5, 10, 15, 30];
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ViewTargetSetup>.reactive(
viewModelBuilder: () => ViewTargetSetup(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, false);
}
void _onRoutinesDown() {
setState(() {
if (_targets.routinesPerWeek > _routineFloor) {
_targets.routinesPerWeek = _targets.routinesPerWeek - 1;
}
});
}
void _onRoutinesUp() {
setState(() {
if (_targets.routinesPerWeek < _routineCeiling) {
_targets.routinesPerWeek = _targets.routinesPerWeek + 1;
}
});
}
void _onMatchSchedule() {
setState(() {
_targets.routinesPerWeek =
_scheduled.clamp(_routineFloor, _routineCeiling);
});
}
void _onMinutesSelected(int minutes) {
setState(() {
_targets.minutesPerWeek = minutes;
});
}
void _onToggleReminders() {
setState(() {
_targets.reminders = !_targets.reminders;
});
}
void _onLeadSelected(int lead) {
setState(() {
_targets.reminderLead = lead;
});
}
void _onSave() {
_model?.save(_targets);
}
// ── Views ─────────────────────────────────────────────────────────────────
Widget _mobileView(BoxConstraints constraints) {
return Sheet(
eyebrow: "Progress",
title: "Targets",
onBack: _onBack,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
displayTitle("What counts as\na good week?"),
const SizedBox(height: 10),
text(
"Progress measures against these. Debt doesn't: missing a target costs nothing, only missing a commitment does.",
13,
TextType.Regular,
color: colorInkSoft,
height: 1.5,
),
const SizedBox(height: 28),
sectionBreak("Routines held", caption: "per week"),
_routineStepper(),
const SizedBox(height: 28),
sectionBreak("Active time", caption: "minutes per week, in routines"),
segmentedSelector<int>(
options: _minuteOptions,
selected: _targets.minutesPerWeek,
label: _minuteLabel,
onSelected: _onMinutesSelected,
activeColor: colorNight,
),
const SizedBox(height: 28),
sectionBreak("Reminders", caption: "before each scheduled routine"),
_reminderToggle(),
if (_targets.reminders) ...[
const SizedBox(height: 14),
segmentedSelector<int>(
options: _leadOptions,
selected: _targets.reminderLead,
label: _leadLabel,
onSelected: _onLeadSelected,
activeColor: colorNight,
),
],
const SizedBox(height: 10),
text(
kIsWeb
? "Reminders use the phone's notifications, so they only fire in the installed app."
: "Uses this phone's notifications. Non-negotiables still get their full-screen alarm when the window closes.",
12,
TextType.Regular,
color: colorGrey2,
height: 1.5,
),
const SizedBox(height: 32),
roundedCornerButton(
"Save targets",
_onSave,
icon: CupertinoIcons.checkmark,
),
const SizedBox(height: 12),
],
),
);
}
String _minuteLabel(int minutes) => formatMinutes(minutes);
String _leadLabel(int lead) => lead == 0 ? "At start" : "${lead}m before";
Widget _routineStepper() {
return card(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
_stepButton(CupertinoIcons.minus, _onRoutinesDown,
_targets.routinesPerWeek > _routineFloor),
Expanded(
child: Column(
children: [
text("${_targets.routinesPerWeek}", 44, TextType.Light,
color: colorInk, height: 1),
const SizedBox(height: 4),
text(
"about ${(_targets.routinesPerWeek / 7).toStringAsFixed(1)} a day",
12,
TextType.Regular,
color: colorInkSoft,
),
],
),
),
_stepButton(CupertinoIcons.add, _onRoutinesUp,
_targets.routinesPerWeek < _routineCeiling),
],
),
if (_scheduled > 0) ...[
const SizedBox(height: 16),
Container(height: 1, color: colorDivider),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: text(
"Your schedule asks for $_scheduled a week.",
12,
TextType.Regular,
color: colorInkSoft,
),
),
if (_scheduled != _targets.routinesPerWeek)
GestureDetector(
onTap: _onMatchSchedule,
child:
text("Match it", 12, TextType.Medium, color: colorInk),
),
],
),
],
],
),
);
}
Widget _stepButton(IconData icon, VoidCallback onTap, bool enabled) {
return GestureDetector(
onTap: enabled ? onTap : null,
child: Container(
width: 48,
height: 48,
alignment: Alignment.center,
decoration: BoxDecoration(
color: enabled ? colorNight : colorMuted,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Icon(icon, size: 20, color: enabled ? colorLime : colorGrey),
),
);
}
Widget _reminderToggle() {
final bool on = _targets.reminders;
return card(
onTap: _onToggleReminders,
background: on ? colorStandingGoodBg : colorCard,
borderColor: on ? colorPositive.withValues(alpha: 0.2) : colorBorder,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
text(on ? "On" : "Off", 16, TextType.Medium,
color: colorPrimaryDark),
const SizedBox(height: 6),
text(
on
? "A heads-up before every scheduled routine. Tap to turn off."
: "No heads-up before routines. Tap to turn on.",
12,
TextType.Regular,
color: colorGrey2,
),
],
),
),
Icon(
on ? CupertinoIcons.bell_fill : CupertinoIcons.bell_slash,
size: 24,
color: on ? colorPositive : colorGrey2,
),
],
),
);
}
// ── ConnectTargetSetup ────────────────────────────────────────────────────
@override
void onTargetsLoaded(ProgressTargets targets, int scheduledPerWeek) {
setState(() {
_targets = targets;
_scheduled = scheduledPerWeek;
});
}
@override
void onTargetsSaved() {
Navigator.pop(context, true);
}
}