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
305 lines
9.2 KiB
Dart
305 lines
9.2 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,
|
|
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);
|
|
}
|
|
}
|