Add Progress over time and a targets and reminders setup
Progress gives a Day/Week/Month view of routines held against a weekly target, compared with the same point of the previous period, with active time and the daily streak. Targets & reminders sets routines and minutes per week and schedules a heads-up before each routine. The runner's summary now shows today's held count and streak. Finished runs are mirrored to a device run log that feeds both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
304
frontend/lib/Grounded/see/progress/TargetSetupState.dart
Normal file
304
frontend/lib/Grounded/see/progress/TargetSetupState.dart
Normal file
@@ -0,0 +1,304 @@
|
||||
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,
|
||||
radius: 0,
|
||||
),
|
||||
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,
|
||||
color: enabled ? colorNight : colorMuted,
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user