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

247 lines
8.0 KiB
Dart

import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart';
import '../../about/external/data/Goal.dart';
import '../../about/internal/application/TextType.dart';
import '../../configs/Navigator.dart';
import '../../designs/Component.dart';
import '../../designs/GlassSurface.dart';
import '../../designs/Responsive.dart';
import '../../designs/Shell.dart';
import '../../designs/buttons/Buttons.dart';
import '../../designs/scene/SceneArt.dart';
import '../../designs/text/Text.dart';
import '../../utils/Colors.dart';
import '../../utils/CommonUtils.dart';
import 'ConnectGoals.dart';
import 'GoalDetail.dart';
import 'GoalEditor.dart';
import 'Goals.dart';
import 'ViewGoals.dart';
class GoalsState extends State<Goals> implements ConnectGoals {
ViewGoals? _model;
List<Goal> _goals = <Goal>[];
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ViewGoals>.reactive(
viewModelBuilder: () => ViewGoals(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?.loadGoals();
}
void _onBack() {
Navigator.pop(context);
}
void _onOpenGoal(Goal goal) async {
final result = await GroundedNavigation()
.navigateToPageWithData(GoalDetail(goal: goal), context);
if (result == true) {
_model?.loadGoals();
}
}
void _onNewGoal() async {
final result = await GroundedNavigation()
.navigateToPageWithData(const GoalEditor(), context);
if (result is Goal) {
_model?.loadGoals();
}
}
Widget _mobileView(BoxConstraints constraints) {
return Sheet(
eyebrow: "Grounded",
title: "Goals",
onBack: widget.tab ? null : _onBack,
bottomSpace: widget.tab ? 130 : 40,
action: chromeAction(CupertinoIcons.add, _onNewGoal),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
displayTitle("What you are\nworking toward."),
const SizedBox(height: 14),
text(
"A goal holds the tasks that get you there. The goal never carries debt — the tasks inside it do.",
14,
TextType.Regular,
color: colorGrey2,
height: 1.55,
),
const SizedBox(height: 28),
if (_goals.isEmpty)
emptyState(
CupertinoIcons.flag,
"No goals yet",
"Create one — Workout, say — then put the actual sessions inside it.",
)
else
..._goals.map(_goalCard),
const SizedBox(height: 24),
roundedCornerButton("New goal", _onNewGoal,
icon: CupertinoIcons.add),
],
),
);
}
/// The goal as a painted card in its own colour: the ambition up top, the
/// progress and the cost along the bottom.
Widget _goalCard(Goal goal) {
final Color accent = goalColor(goal.colourHex);
final Color soft = colorWhite.withValues(alpha: 0.78);
final bool overdue = goal.targetDate != null &&
goal.targetDate!.isBefore(DateTime.now());
return GestureDetector(
onTap: () => _onOpenGoal(goal),
child: Container(
height: 260,
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x262A3A30),
blurRadius: 24,
offset: Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
fit: StackFit.expand,
children: [
SceneArt(
palette: goalScene(accent),
seed: sceneSeed(goal.title),
figure: CupertinoIcons.flag_fill,
figureScale: 0.4,
figureAlignment: const Alignment(0.75, -0.35),
shade: true,
),
Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
GlassSurface(
onDark: true,
radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
child: text(
[
if (goal.category.isNotEmpty) goal.category,
goal.targetDate == null
? "No deadline"
: targetLabel(goal.targetDate),
].join(" · "),
10,
TextType.Medium,
color: overdue ? colorTinted : colorWhite,
),
),
const Spacer(),
if (goal.overdueTasks > 0)
pill("${goal.overdueTasks} late", colorWhite,
colorStandingGrounded,
textSize: 9),
],
),
const Spacer(),
text(goal.title, 21, TextType.Medium,
color: colorWhite,
maxLines: 1,
overflow: TextOverflow.ellipsis),
if (goal.description.isNotEmpty) ...[
const SizedBox(height: 3),
text(goal.description, 12, TextType.Regular,
color: soft,
maxLines: 1,
overflow: TextOverflow.ellipsis),
],
const SizedBox(height: 12),
meter(goal.progress,
fill: colorLime,
track: colorWhite.withValues(alpha: 0.25),
height: 5),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _cardStat(
"${goal.completedTasks}/${goal.totalTasks}",
"Done",
soft),
),
Expanded(
child: _cardStat(
"${goal.remainingTasks}", "Remaining", soft),
),
Expanded(
child: _cardStat(
formatDebt(goal.debtContribution), "Debt", soft,
alert: goal.debtContribution > 0),
),
],
),
],
),
),
],
),
),
),
);
}
Widget _cardStat(String value, String caption, Color tone,
{bool alert = false}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
text(value, 18, TextType.Light,
color: alert ? colorTinted : colorWhite),
const SizedBox(width: 5),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: text(caption, 10, TextType.Regular, color: tone),
),
],
);
}
@override
void onGoalsLoaded(List<Goal> goals) {
setState(() {
_goals = goals;
});
}
}