Add Coach, take the reference's colours and copy its nav and motion

Coach is the centre tab: questions about routines, slips, streak, today,
active time and debt, answered on the device from the user's own data,
with a target bar chart and buttons that start routines or open the right
screen. The palette moves to neutral grey, near-black and one orange
accent, and Home's header becomes a light panel. The nav is now
overlapping frosted circles whose active disc pops forward, and tabs fade
and rise in on switch. Goals leaves the nav for Coach and stays one tap
away in Home's category row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
alvocool
2026-11-09 07:22:35 +03:00
parent d3431ca5de
commit 4cd3f6c038
18 changed files with 1688 additions and 169 deletions

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'CoachState.dart';
class Coach extends StatefulWidget {
/// Shown as a tab inside Home: no back arrow, and room at the bottom
/// for the nav.
final bool tab;
const Coach({super.key, this.tab = false});
@override
State<Coach> createState() => CoachState();
}

View File

@@ -0,0 +1,562 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:stacked/stacked.dart';
import '../../about/external/data/RoutineChain.dart';
import '../../about/internal/application/CoachAction.dart';
import '../../about/internal/application/CoachActionKind.dart';
import '../../about/internal/application/CoachChart.dart';
import '../../about/internal/application/CoachMessage.dart';
import '../../about/internal/application/TextType.dart';
import '../../configs/Navigator.dart';
import '../../designs/DashedOutline.dart';
import '../../designs/GlassSurface.dart';
import '../../designs/Responsive.dart';
import '../../designs/AmbientBackground.dart';
import '../../designs/input/InputFields.dart';
import '../../designs/text/Text.dart';
import '../../utils/CoachEngine.dart';
import '../../utils/Colors.dart';
import '../../utils/CommonUtils.dart';
import '../overdue/OverdueQueue.dart';
import '../progress/Progress.dart';
import '../progress/TargetSetup.dart';
import '../routine/RoutineRunner.dart';
import 'Coach.dart';
import 'ConnectCoach.dart';
import 'ViewCoach.dart';
class CoachState extends State<Coach> implements ConnectCoach {
ViewCoach? _model;
final List<CoachMessage> _thread = <CoachMessage>[];
final TextEditingController _input = TextEditingController();
final FocusNode _focus = FocusNode();
final ScrollController _scroll = ScrollController();
bool _thinking = false;
static const double _barHeight = 96;
@override
void dispose() {
_input.dispose();
_focus.dispose();
_scroll.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ViewCoach>.reactive(
viewModelBuilder: () => ViewCoach(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() {}
// ── Handlers ──────────────────────────────────────────────────────────────
void _onBack() {
Navigator.pop(context);
}
void _onNewChat() {
setState(() {
_thread.clear();
_thinking = false;
});
}
void _onSend() {
_send(_input.text);
}
void _onSubmitted(String value) {
_send(value);
}
void _send(String question) {
final String trimmed = question.trim();
if (trimmed.isEmpty || _thinking) {
return;
}
_input.clear();
setState(() {
_thread.add(ViewCoach.question(trimmed));
_thinking = true;
});
_toBottom();
_model?.ask(trimmed);
}
void _onAction(CoachAction action) async {
switch (action.kind) {
case CoachActionKind.Ask:
_send(action.question);
return;
case CoachActionKind.OpenOverdue:
await GroundedNavigation()
.navigateToPageWithData(const OverdueQueue(), context);
return;
case CoachActionKind.OpenTargets:
await GroundedNavigation()
.navigateToPageWithData(const TargetSetup(), context);
return;
case CoachActionKind.OpenProgress:
await GroundedNavigation()
.navigateToPageWithData(const Progress(), context);
return;
case CoachActionKind.StartRoutine:
final RoutineChain? routine =
await _model?.findRoutine(action.routineId);
if (routine == null || !mounted) {
return;
}
await GroundedNavigation().navigateToPageWithData(
RoutineRunner(routine: routine.copy()), context);
return;
}
}
void _toBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_scroll.hasClients) {
return;
}
_scroll.animateTo(
_scroll.position.maxScrollExtent,
duration: const Duration(milliseconds: 350),
curve: Curves.easeOutCubic,
);
});
}
// ── Views ─────────────────────────────────────────────────────────────────
Widget _mobileView(BoxConstraints constraints) {
return Scaffold(
backgroundColor: colorAmbientMid,
resizeToAvoidBottomInset: true,
body: AmbientBackground(
child: SafeArea(
bottom: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_header(),
Expanded(
child: _thread.isEmpty ? _intro() : _threadView(),
),
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, widget.tab ? 104 : 20),
child: _composer(),
),
],
),
),
),
);
}
Widget _header() {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 14, 16, 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!widget.tab) ...[
glassCircle(CupertinoIcons.arrow_left, _onBack, size: 46),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
text("Coach", 32, TextType.Medium,
color: colorInk, letterSpacing: -0.6),
const SizedBox(height: 2),
text("Answers from your own data", 15, TextType.Regular,
color: colorInkSoft),
],
),
),
Tooltip(
message: "New chat",
child: glassCircle(
CupertinoIcons.arrow_counterclockwise, _onNewChat,
size: 50),
),
],
),
);
}
/// Before the first question: what the coach can answer, as one-tap asks.
Widget _intro() {
return ListView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 20),
children: [
text(
"Ask about your routines, streak, time or debt. Every answer is worked out from what you've logged — nothing is guessed.",
15,
TextType.Regular,
color: colorInkSoft,
height: 1.5,
),
const SizedBox(height: 22),
for (int index = 0; index < CoachEngine.suggestions.length; index++)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _suggestion(CoachEngine.suggestions[index]),
).animate().fadeIn(duration: 300.ms, delay: (60 * index).ms).slideY(
begin: 0.15, end: 0, duration: 300.ms, delay: (60 * index).ms),
],
);
}
Widget _suggestion(String question) {
return GestureDetector(
onTap: () => _send(question),
child: Container(
padding: const EdgeInsets.fromLTRB(18, 16, 14, 16),
decoration: BoxDecoration(
color: colorGlassStrong,
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(color: colorGlassBorder),
),
child: Row(
children: [
Icon(CupertinoIcons.sparkles, size: 18, color: colorAccent),
const SizedBox(width: 12),
Expanded(
child: text(question, 15, TextType.Medium, color: colorInk),
),
Icon(CupertinoIcons.arrow_up_right, size: 16, color: colorInkSoft),
],
),
),
);
}
Widget _threadView() {
return ListView.builder(
controller: _scroll,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
itemCount: _thread.length + 1 + (_thinking ? 1 : 0),
itemBuilder: (context, index) {
if (index == 0) {
return _stamp(_thread.first.at);
}
if (index - 1 < _thread.length) {
final CoachMessage message = _thread[index - 1];
return (message.mine ? _question(message) : _answer(message))
.animate()
.fadeIn(duration: 320.ms)
.slideY(
begin: 0.12,
end: 0,
duration: 380.ms,
curve: Curves.easeOutCubic);
}
return _typing();
},
);
}
Widget _stamp(DateTime at) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Center(
child: text("Today ${formatStartMinute(at.hour * 60 + at.minute)}", 13,
TextType.Regular,
color: colorInkSoft),
),
);
}
Widget _question(CoachMessage message) {
return Align(
alignment: Alignment.centerRight,
child: Container(
margin: const EdgeInsets.only(left: 60, bottom: 14),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 13),
decoration: const BoxDecoration(
color: colorNight,
borderRadius: BorderRadius.all(Radius.circular(16)),
),
child: text(message.body, 15, TextType.Medium, color: colorWhite),
),
);
}
Widget _answer(CoachMessage message) {
return Container(
margin: const EdgeInsets.only(bottom: 14),
padding: const EdgeInsets.fromLTRB(18, 18, 18, 18),
decoration: BoxDecoration(
color: colorGlassStrong,
borderRadius: const BorderRadius.all(Radius.circular(18)),
border: Border.all(color: colorGlassBorder),
boxShadow: const [
BoxShadow(
color: Color(0x0F000000),
blurRadius: 24,
offset: Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
markedText(message.body, 15, color: colorInk, height: 1.5),
if (message.chart != null) ...[
const SizedBox(height: 16),
_chart(message.chart!),
],
if (message.followUp.isNotEmpty) ...[
const SizedBox(height: 16),
markedText(message.followUp, 15, color: colorInk, height: 1.5),
],
if (message.actions.isNotEmpty) ...[
const SizedBox(height: 18),
Wrap(
spacing: 8,
runSpacing: 8,
children: message.actions.map(_actionButton).toList(),
),
],
],
),
);
}
Widget _actionButton(CoachAction action) {
return GestureDetector(
onTap: () => _onAction(action),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
decoration: BoxDecoration(
color: action.primary ? colorAccent : colorWhite,
borderRadius: const BorderRadius.all(Radius.circular(14)),
border: Border.all(color: action.primary ? colorAccent : colorBorder),
),
child: text(action.label, 14, TextType.Medium,
color: action.primary ? colorWhite : colorInk),
),
);
}
/// Bars against dashed target outlines. Days met in solid accent, days
/// short in a soft tint, the day still running as an outline only.
Widget _chart(CoachChart chart) {
double peak = 1;
for (int index = 0; index < chart.values.length; index++) {
peak = [peak, chart.values[index], chart.targets[index]]
.reduce((a, b) => a > b ? a : b);
}
return Container(
padding: const EdgeInsets.fromLTRB(14, 14, 14, 12),
decoration: BoxDecoration(
color: colorWhite.withValues(alpha: 0.6),
borderRadius: const BorderRadius.all(Radius.circular(14)),
border: Border.all(color: colorGlassBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
text(chart.title, 12, TextType.Regular, color: colorInkSoft),
const SizedBox(height: 12),
SizedBox(
height: _barHeight + 50,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: List<Widget>.generate(chart.values.length, (index) {
return Expanded(child: _bar(chart, index, peak));
}),
),
),
],
),
);
}
Widget _bar(CoachChart chart, int index, double peak) {
final double value = chart.values[index];
final double target = chart.targets[index];
final bool running = index > chart.lastComplete;
final bool met = target <= 0 || value >= target;
final double targetHeight = _barHeight * (target / peak);
final double valueHeight = _barHeight * (value / peak);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (index == chart.highlight && chart.callout.isNotEmpty)
Container(
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: colorWhite,
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(color: colorBorder),
),
child: text(chart.callout, 10, TextType.Medium, color: colorInk),
).animate().fadeIn(duration: 300.ms, delay: 500.ms),
SizedBox(
height: (targetHeight > valueHeight ? targetHeight : valueHeight)
.clamp(8, _barHeight),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
if (target > 0)
Positioned.fill(
top: null,
child: SizedBox(
height: targetHeight.clamp(8, _barHeight),
child: CustomPaint(
painter: DashedOutline(
color: colorAccent.withValues(alpha: 0.55)),
),
),
),
if (!running)
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: valueHeight),
duration: Duration(milliseconds: 600 + index * 60),
curve: Curves.easeOutCubic,
builder: (context, height, child) => Container(
height: height,
decoration: BoxDecoration(
color: met ? colorAccent : colorAccentSoft,
borderRadius:
const BorderRadius.all(Radius.circular(10)),
),
),
),
],
),
),
const SizedBox(height: 8),
text(chart.labels[index], 11, TextType.Regular,
color: colorInkSoft, maxLines: 1),
],
),
);
}
Widget _typing() {
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 14),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
decoration: BoxDecoration(
color: colorGlassStrong,
borderRadius: const BorderRadius.all(Radius.circular(18)),
border: Border.all(color: colorGlassBorder),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List<Widget>.generate(3, (index) {
return Container(
width: 7,
height: 7,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: const BoxDecoration(
color: colorInkSoft,
shape: BoxShape.circle,
),
)
.animate(onPlay: (controller) => controller.repeat())
.fadeOut(duration: 450.ms, delay: (150 * index).ms)
.then()
.fadeIn(duration: 450.ms);
}),
),
),
);
}
/// The ask bar, as in the reference: a frosted field with the send button
/// as a solid accent disc.
Widget _composer() {
return Container(
padding: const EdgeInsets.fromLTRB(18, 6, 6, 6),
decoration: BoxDecoration(
color: colorGlassStrong,
borderRadius: const BorderRadius.all(Radius.circular(20)),
border: Border.all(color: colorGlassBorder),
boxShadow: const [
BoxShadow(
color: Color(0x0F000000),
blurRadius: 20,
offset: Offset(0, 6),
),
],
),
child: Row(
children: [
Expanded(
child: quickAddField(
_input,
"Ask about routines, streaks, debt",
_onSubmitted,
focusNode: _focus,
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _onSend,
child: Container(
width: 50,
height: 50,
decoration: const BoxDecoration(
color: colorAccent,
shape: BoxShape.circle,
),
child: Icon(CupertinoIcons.arrow_up, size: 22, color: colorWhite),
),
),
],
),
);
}
// ── ConnectCoach ──────────────────────────────────────────────────────────
@override
void onAnswered(CoachMessage answer) {
if (!mounted) {
return;
}
setState(() {
_thinking = false;
_thread.add(answer);
});
_toBottom();
}
}

View File

@@ -0,0 +1,5 @@
import '../../about/internal/application/CoachMessage.dart';
abstract class ConnectCoach {
void onAnswered(CoachMessage answer);
}

View File

@@ -0,0 +1,107 @@
import '../../about/external/data/Commitment.dart';
import '../../about/external/data/RoutineChain.dart';
import '../../about/external/data/pages/request/CommitmentsRequest.dart';
import '../../about/external/data/pages/request/HistoryRequest.dart';
import '../../about/external/data/pages/request/PageAndSort.dart';
import '../../about/external/data/pages/request/Pageable.dart';
import '../../about/external/data/pages/request/Sort.dart';
import '../../about/external/data/pages/response/CommitmentPage.dart';
import '../../about/internal/application/CoachContext.dart';
import '../../about/internal/application/CoachMessage.dart';
import '../../utils/CoachEngine.dart';
import '../../utils/ObjectConvertors.dart';
import '../parent/ParentViewModel.dart';
import 'ConnectCoach.dart';
class ViewCoach extends ParentViewModel {
ConnectCoach connection;
ViewCoach(super.context, this.connection);
/// The coach reads fresh data for every question, so an answer never
/// lags behind a routine just logged. Network parts fail quietly: the run
/// log on the device still answers most questions.
void ask(String question) async {
final CoachContext data = await _gather();
// A short beat so the answer reads as considered, not instant.
await Future<void>.delayed(const Duration(milliseconds: 700));
connection.onAnswered(CoachEngine.answer(question, data, DateTime.now()));
}
Future<CoachContext> _gather() async {
final PageAndSort page = PageAndSort(
sort: Sort('asc', 'startMinute'),
page: Pageable(0, 0, 50, 0),
);
List<RoutineChain> routines = <RoutineChain>[];
try {
final response =
await getDataManager().getMyRoutines(HistoryRequest(query: page));
routines = getRoutineChainList(response.data);
} catch (e) {
routines = <RoutineChain>[];
}
List<Commitment> plan = <Commitment>[];
try {
final response = await getDataManager().getTodayPlan(CommitmentsRequest(
query: PageAndSort(
sort: Sort('asc', 'dueStart'),
page: Pageable(0, 0, 50, 0),
),
));
plan = CommitmentPage.fromJson(response.data).content;
} catch (e) {
plan = <Commitment>[];
}
List<Commitment> overdue = <Commitment>[];
try {
final response = await getDataManager().getOverdueQueue(HistoryRequest(
query: PageAndSort(
sort: Sort('desc', 'dueEnd'),
page: Pageable(0, 0, 50, 0),
),
));
overdue = CommitmentPage.fromJson(response.data).content;
} catch (e) {
overdue = <Commitment>[];
}
return CoachContext(
runs: await getDataManager().getRunLog(),
routines: routines,
targets: await getDataManager().getProgressTargets(),
plan: plan,
overdue: overdue,
debtScore: await getDataManager().getCachedDebtScore(),
);
}
/// The routine a "Start" button names, fetched fresh so it runs current
/// steps.
Future<RoutineChain?> findRoutine(String id) async {
try {
final response = await getDataManager().getMyRoutines(HistoryRequest(
query: PageAndSort(
sort: Sort('asc', 'startMinute'),
page: Pageable(0, 0, 50, 0),
),
));
for (RoutineChain routine in getRoutineChainList(response.data)) {
if (routine.id == id) {
return routine;
}
}
} catch (e) {
return null;
}
return null;
}
static CoachMessage question(String text) =>
CoachMessage(mine: true, body: text);
}

View File

@@ -41,6 +41,7 @@ import '../../utils/Thresholds.dart';
import '../../utils/ToneEngine.dart';
import '../commitment/NewCommitment.dart';
import '../excuse/ExcuseReport.dart';
import '../coach/Coach.dart';
import '../goal/Goals.dart';
import '../progress/Progress.dart';
import '../group/GroupChat.dart';
@@ -84,6 +85,9 @@ class HomeState extends State<Home> implements ConnectHome {
/// The open tab, and the tabs built so far.
int _tab = 0;
/// Counts tab switches; drives the fade-in of the incoming tab.
int _switches = 0;
final Set<int> _opened = <int>{0};
Standing _standing = Standing.Good;
@@ -192,7 +196,14 @@ class HomeState extends State<Home> implements ConnectHome {
NavigatorType.justOpen, const ReportCardScreen(), context);
}
void _onOpenGoals() => _onNavGoals();
void _onOpenGoals() async {
final result = await GroundedNavigation()
.navigateToPageWithData(const Goals(), context);
if (result == true) {
_model?.loadPlan();
}
}
void _onOpenGroups() => _onNavGroups();
@@ -255,6 +266,7 @@ class HomeState extends State<Home> implements ConnectHome {
setState(() {
_tab = index;
_opened.add(index);
_switches = _switches + 1;
});
// Back on Today: pick up whatever changed in the other tabs.
@@ -268,7 +280,7 @@ class HomeState extends State<Home> implements ConnectHome {
void _onNavRoutines() => _onSelectTab(1);
void _onNavGoals() => _onSelectTab(2);
void _onNavCoach() => _onSelectTab(2);
void _onNavGroups() => _onSelectTab(3);
@@ -412,29 +424,43 @@ class HomeState extends State<Home> implements ConnectHome {
canPop: _tab == 0,
onPopInvokedWithResult: _onBackToToday,
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
value: SystemUiOverlayStyle.dark,
child: Scaffold(
backgroundColor: colorAmbientMid,
body: AmbientBackground(
child: Stack(
children: [
IndexedStack(
index: _tab,
children: <Widget>[
_todayView(queueIsHome),
_opened.contains(1)
? const Routines(tab: true)
: const SizedBox.shrink(),
_opened.contains(2)
? const Goals(tab: true)
: const SizedBox.shrink(),
_opened.contains(3)
? const Groups(tab: true)
: const SizedBox.shrink(),
_opened.contains(4)
? const Progress(tab: true)
: const SizedBox.shrink(),
],
TweenAnimationBuilder<double>(
// Animates from the previous switch count to the new one, so
// the tabs underneath are never rebuilt and keep their state.
tween: Tween<double>(end: _switches.toDouble()),
duration: const Duration(milliseconds: 280),
curve: Curves.easeOutCubic,
builder: (context, value, child) => Opacity(
opacity: (1 - (_switches - value)).clamp(0.0, 1.0),
child: Transform.translate(
offset: Offset(0, 14 * (_switches - value)),
child: child,
),
),
child: IndexedStack(
index: _tab,
children: <Widget>[
_todayView(queueIsHome),
_opened.contains(1)
? const Routines(tab: true)
: const SizedBox.shrink(),
_opened.contains(2)
? const Coach(tab: true)
: const SizedBox.shrink(),
_opened.contains(3)
? const Groups(tab: true)
: const SizedBox.shrink(),
_opened.contains(4)
? const Progress(tab: true)
: const SizedBox.shrink(),
],
),
),
Positioned(
left: 0,
@@ -444,27 +470,27 @@ class HomeState extends State<Home> implements ConnectHome {
selected: _tab,
entries: <NavEntry>[
(
icon: CupertinoIcons.house_fill,
icon: CupertinoIcons.house,
label: "Today",
onTap: _onNavToday
),
(
icon: CupertinoIcons.square_stack_3d_up_fill,
icon: CupertinoIcons.square_stack_3d_up,
label: "Routines",
onTap: _onNavRoutines
),
(
icon: CupertinoIcons.flag_fill,
label: "Goals",
onTap: _onNavGoals
icon: CupertinoIcons.sparkles,
label: "Coach",
onTap: _onNavCoach
),
(
icon: CupertinoIcons.person_3_fill,
icon: CupertinoIcons.person_3,
label: "Groups",
onTap: _onNavGroups
),
(
icon: CupertinoIcons.chart_bar_alt_fill,
icon: CupertinoIcons.chart_pie,
label: "Progress",
onTap: _onNavProgress
),
@@ -578,12 +604,12 @@ class HomeState extends State<Home> implements ConnectHome {
text(_greetingWord(), 14, TextType.Regular,
color: colorHeaderMuted),
const SizedBox(width: 6),
Icon(_greetingIcon(), size: 15, color: colorLime),
Icon(_greetingIcon(), size: 15, color: colorAccent),
],
),
const SizedBox(height: 3),
text(name, 22, TextType.Medium,
color: colorWhite,
color: colorHeaderText,
maxLines: 1,
overflow: TextOverflow.ellipsis),
],
@@ -593,7 +619,7 @@ class HomeState extends State<Home> implements ConnectHome {
CupertinoIcons.bell,
_onOpenOverdue,
size: 46,
onDark: true,
onDark: false,
dotted: _overdue.isNotEmpty,
dotColor: colorBarNonNegotiable,
),
@@ -602,7 +628,7 @@ class HomeState extends State<Home> implements ConnectHome {
CupertinoIcons.person,
_onOpenSettings,
size: 46,
onDark: true,
onDark: false,
dotted: _user.sickMode,
dotColor: colorBarStandard,
),
@@ -643,7 +669,7 @@ class HomeState extends State<Home> implements ConnectHome {
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: text(label, 16, active ? TextType.Medium : TextType.Regular,
color: active ? colorWhite : colorHeaderMuted),
color: active ? colorHeaderText : colorHeaderMuted),
),
);
}
@@ -678,11 +704,11 @@ class HomeState extends State<Home> implements ConnectHome {
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
text("${_streak()}", 28, TextType.Medium, color: colorWhite),
text("${_streak()}", 28, TextType.Medium, color: colorHeaderText),
const SizedBox(width: 8),
text("day streak", 16, TextType.Regular, color: colorHeaderMuted),
const Spacer(),
Icon(Icons.eco, size: 32, color: colorLime),
Icon(Icons.eco, size: 32, color: colorAccent),
],
),
const SizedBox(height: 16),
@@ -709,7 +735,7 @@ class HomeState extends State<Home> implements ConnectHome {
mainAxisSize: MainAxisSize.min,
children: [
text(label, 14, today ? TextType.Medium : TextType.Regular,
color: today ? colorWhite : colorHeaderMuted),
color: today ? colorHeaderText : colorHeaderMuted),
const SizedBox(height: 10),
SizedBox(
height: 26,
@@ -718,7 +744,7 @@ class HomeState extends State<Home> implements ConnectHome {
? Icon(Icons.eco, size: 24, color: colorBarElective)
: text("${date.day}", 16,
today ? TextType.Medium : TextType.Regular,
color: today ? colorWhite : colorHeaderMuted),
color: today ? colorHeaderText : colorHeaderMuted),
),
),
],
@@ -758,7 +784,8 @@ class HomeState extends State<Home> implements ConnectHome {
stroke: 4,
fill: colorBarElective,
track: colorHeaderLine,
child: Icon(CupertinoIcons.add, size: 30, color: colorWhite),
child:
Icon(CupertinoIcons.add, size: 30, color: colorHeaderText),
),
),
],
@@ -854,7 +881,7 @@ class HomeState extends State<Home> implements ConnectHome {
? TextType.Medium
: TextType.Regular,
color: day == now.weekday
? colorWhite
? colorHeaderText
: colorHeaderMuted),
],
),
@@ -883,7 +910,8 @@ class HomeState extends State<Home> implements ConnectHome {
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
text(value, 38, TextType.Medium, color: colorWhite, letterSpacing: -1),
text(value, 38, TextType.Medium,
color: colorHeaderText, letterSpacing: -1),
const SizedBox(width: 4),
Flexible(
child: text(unit, 16, TextType.Regular,
@@ -927,7 +955,7 @@ class HomeState extends State<Home> implements ConnectHome {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
text(grade, 46, TextType.Medium, color: colorWhite, height: 1),
text(grade, 46, TextType.Medium, color: colorHeaderText, height: 1),
const SizedBox(height: 2),
text("Score", 16, TextType.Regular, color: colorHeaderMuted),
],
@@ -960,7 +988,7 @@ class HomeState extends State<Home> implements ConnectHome {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
text(label, 14, TextType.Medium, color: colorWhite, maxLines: 1),
text(label, 14, TextType.Medium, color: colorHeaderText, maxLines: 1),
const SizedBox(height: 9),
Container(
height: 7,
@@ -983,10 +1011,10 @@ class HomeState extends State<Home> implements ConnectHome {
/// Standing, debt and overdue in one quiet line under the numbers.
Widget _standingLine() {
final Color tone = _standing == Standing.Good
? colorBarElective
? colorStandingGood
: _standing == Standing.Warned
? colorBarStandard
: colorBarNonNegotiable;
? colorStandingWarned
: colorStandingGrounded;
return GestureDetector(
onTap: _overdue.isEmpty ? _onOpenReportCard : _onOpenOverdue,