Files
GroundedHelper/frontend/lib/Grounded/see/goal/GoalsState.dart
alvocool 9c858b8146 Square every corner, enlarge the main surfaces and add a to-do list
Rounded rectangles and round controls are now square across the app; only
status dots, glows, the sun, rings and the breathing circle stay round.
Cards, rows, thumbnails, featured cards, category tiles, the standing card
and the nav are larger. Home gets a quick To-do list kept on the device
(TodoItem in InternalMemory), seeded with sample items in mock mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-02 14:50:31 +03:00

246 lines
7.9 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: _onBack,
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.zero,
boxShadow: const [
BoxShadow(
color: Color(0x262A3A30),
blurRadius: 24,
offset: Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.zero,
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.zero,
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;
});
}
}