Files
GroundedHelper/frontend/lib/Grounded/see/goal/GoalsState.dart
alvocool fe268cb8aa Redesign the UI after the fitness reference and replace the logo
Light sage-to-cream ambient backdrop, frosted glass controls and painted
SceneArt landscapes in place of photography. Home becomes a dashboard with a
two-tone greeting, a frosted summary ring, category portraits, an up-next rail
of image-led cards and a floating glass tab bar. The routine library gets an
image header and painted cards; the player gets a scene stage, steps on an
arc, a dotted clock and a night pill with a lime spark. Goals become painted
cards in their own colour.

New logo: a lime sun resting on a ground line under two arcs. The splash
raises the sun and lights the arcs; launcher icons and native splash are
regenerated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-02 12:33:13 +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: 214,
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(26),
boxShadow: const [
BoxShadow(
color: Color(0x262A3A30),
blurRadius: 24,
offset: Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(26),
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(99),
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;
});
}
}