Files
GroundedHelper/frontend/lib/Grounded/utils/CommonUtils.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

407 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../about/external/data/Commitment.dart';
import '../about/external/data/RoutineStep.dart';
import '../about/internal/application/CommitmentClass.dart';
import '../about/internal/application/RoutineKind.dart';
import '../about/internal/application/Standing.dart';
import '../about/internal/application/StepMode.dart';
import 'Colors.dart';
/// Formatting and small shared derivations. Anything a screen would otherwise
/// inline twice belongs here.
/// "Mon 06:00–08:00" — the window, which is the whole point.
String formatWindow(Commitment commitment) {
if (commitment.dueStart == null || commitment.dueEnd == null) {
return "No window set";
}
final DateFormat day = DateFormat('EEE');
final DateFormat time = DateFormat('HH:mm');
final String startDay = day.format(commitment.dueStart!);
final String startTime = time.format(commitment.dueStart!);
final String endTime = time.format(commitment.dueEnd!);
final bool sameDay = commitment.dueStart!.day == commitment.dueEnd!.day &&
commitment.dueStart!.month == commitment.dueEnd!.month;
if (sameDay) {
return "$startDay $startTime–$endTime";
}
return "$startDay $startTime – ${day.format(commitment.dueEnd!)} $endTime";
}
String formatDate(DateTime? value) {
if (value == null) {
return "";
}
return DateFormat('d MMM yyyy').format(value);
}
String formatDateTime(DateTime? value) {
if (value == null) {
return "";
}
return DateFormat('d MMM, HH:mm').format(value);
}
/// "3h 20m" from raw minutes.
String formatMinutes(num minutes) {
final int total = minutes.round();
if (total < 60) {
return "${total}m";
}
final int hours = total ~/ 60;
final int remainder = total % 60;
if (remainder == 0) {
return "${hours}h";
}
return "${hours}h ${remainder}m";
}
/// "12:04" from raw seconds — for the proof timer and rest clocks.
String formatClock(int seconds) {
final int safe = seconds < 0 ? 0 : seconds;
final int minutes = safe ~/ 60;
final int remainder = safe % 60;
return "${minutes.toString().padLeft(2, '0')}:${remainder.toString().padLeft(2, '0')}";
}
/// Debt is shown to one decimal — precise enough to move visibly when you
/// clear something, coarse enough not to look like a lie.
String formatDebt(double debt) {
return debt.toStringAsFixed(1);
}
/// "2 days overdue", "Due in 40m", "Window closes in 3h".
String overdueLabel(Commitment commitment) {
if (commitment.dueEnd == null) {
return "";
}
final Duration difference = DateTime.now().difference(commitment.dueEnd!);
if (difference.isNegative) {
final Duration remaining = difference.abs();
if (remaining.inHours < 1) {
return "Closes in ${remaining.inMinutes}m";
}
if (remaining.inDays < 1) {
return "Closes in ${remaining.inHours}h";
}
return "Closes in ${remaining.inDays}d";
}
if (difference.inHours < 1) {
return "${difference.inMinutes}m overdue";
}
if (difference.inDays < 1) {
return "${difference.inHours}h overdue";
}
if (difference.inDays == 1) {
return "1 day overdue";
}
return "${difference.inDays} days overdue";
}
Color standingColor(Standing standing) {
switch (standing) {
case Standing.Good:
return colorStandingGood;
case Standing.Warned:
return colorStandingWarned;
case Standing.Grounded:
return colorStandingGrounded;
case Standing.Lockdown:
return colorStandingLockdown;
}
}
Color standingBackground(Standing standing) {
switch (standing) {
case Standing.Good:
return colorStandingGoodBg;
case Standing.Warned:
return colorStandingWarnedBg;
case Standing.Grounded:
return colorStandingGroundedBg;
case Standing.Lockdown:
return colorStandingLockdownBg;
}
}
Color classColor(CommitmentClass value) {
switch (value) {
case CommitmentClass.NonNegotiable:
return colorClassNonNegotiable;
case CommitmentClass.Standard:
return colorClassStandard;
case CommitmentClass.Elective:
return colorClassElective;
}
}
Color classBackground(CommitmentClass value) {
switch (value) {
case CommitmentClass.NonNegotiable:
return colorClassNonNegotiableBg;
case CommitmentClass.Standard:
return colorClassStandardBg;
case CommitmentClass.Elective:
return colorClassElectiveBg;
}
}
/// The weekday name for a 1..7 index.
String weekdayName(int weekday) {
const List<String> names = <String>[
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
if (weekday < 1 || weekday > 7) {
return "";
}
return names[weekday - 1];
}
/// "7pm" for an hour-of-day index.
String hourLabel(int hour) {
if (hour < 0) {
return "";
}
if (hour == 0) {
return "midnight";
}
if (hour < 12) {
return "${hour}am";
}
if (hour == 12) {
return "noon";
}
return "${hour - 12}pm";
}
Color kindColor(RoutineKind value) {
switch (value) {
case RoutineKind.Workout:
return colorKindWorkout;
case RoutineKind.Meditation:
return colorKindMeditation;
case RoutineKind.MustDo:
return colorKindMustDo;
}
}
Color kindBackground(RoutineKind value) {
switch (value) {
case RoutineKind.Workout:
return colorKindWorkoutBg;
case RoutineKind.Meditation:
return colorKindMeditationBg;
case RoutineKind.MustDo:
return colorKindMustDoBg;
}
}
/// The dark end of the kind's gradient — featured cards and the runner stage.
Color kindDeep(RoutineKind value) {
switch (value) {
case RoutineKind.Workout:
return colorKindWorkoutDeep;
case RoutineKind.Meditation:
return colorKindMeditationDeep;
case RoutineKind.MustDo:
return colorKindMustDoDeep;
}
}
IconData kindIcon(RoutineKind value) {
switch (value) {
case RoutineKind.Workout:
return Icons.fitness_center_rounded;
case RoutineKind.Meditation:
return Icons.self_improvement_rounded;
case RoutineKind.MustDo:
return CupertinoIcons.checkmark_seal_fill;
}
}
IconData stepModeIcon(StepMode value) {
switch (value) {
case StepMode.Timed:
return CupertinoIcons.timer;
case StepMode.Reps:
return CupertinoIcons.repeat;
case StepMode.Check:
return CupertinoIcons.checkmark_square;
}
}
/// "45s", "2m", "1m 30s" — for step durations, where a clock face would be
/// noise.
String formatSeconds(int seconds) {
final int safe = seconds < 0 ? 0 : seconds;
if (safe < 60) {
return "${safe}s";
}
final int minutes = safe ~/ 60;
final int remainder = safe % 60;
if (remainder == 0) {
return "${minutes}m";
}
return "${minutes}m ${remainder}s";
}
/// "07:30" from minutes after midnight; empty when unset.
String formatStartMinute(int minute) {
if (minute < 0) {
return "";
}
final int hour = minute ~/ 60;
final int rest = minute % 60;
return "${hour.toString().padLeft(2, '0')}:${rest.toString().padLeft(2, '0')}";
}
/// "M T W" style day initials for a 1..7 index.
String weekdayInitial(int weekday) {
const List<String> initials = <String>["M", "T", "W", "T", "F", "S", "S"];
if (weekday < 1 || weekday > 7) {
return "";
}
return initials[weekday - 1];
}
/// "Weekdays", "Every day", "Mon · Wed · Fri" — a schedule read at a glance.
String formatDays(List<int> days) {
if (days.isEmpty) {
return "On demand";
}
final List<int> sorted = List<int>.from(days)..sort();
if (sorted.length == 7) {
return "Every day";
}
if (sorted.length == 5 && sorted.every((day) => day <= 5)) {
return "Weekdays";
}
if (sorted.length == 2 && sorted.contains(6) && sorted.contains(7)) {
return "Weekends";
}
return sorted.map((day) => weekdayName(day).substring(0, 3)).join(" · ");
}
/// "4 × 12 reps · rest 90s" — one line that says what a step asks of you.
String stepSummary(RoutineStep step) {
final List<String> parts = <String>[];
switch (step.mode) {
case StepMode.Timed:
parts.add(step.sets > 1
? "${step.sets} × ${formatSeconds(step.timerSeconds)}"
: formatSeconds(step.timerSeconds));
break;
case StepMode.Reps:
parts.add("${step.sets} × ${step.reps} reps");
break;
case StepMode.Check:
parts.add(step.timerSeconds > 0
? "Tick · ~${formatSeconds(step.timerSeconds)}"
: "Tick");
break;
}
if (step.restSeconds > 0) {
parts.add("rest ${formatSeconds(step.restSeconds)}");
}
return parts.join(" · ");
}
/// A goal's accent, falling back to ink when it has none.
Color goalColor(String colourHex) {
if (colourHex.isEmpty) {
return colorPrimaryDark;
}
return getColorFromHex(colourHex);
}
/// "41 days left", "Due today", "12 days over" — the deadline read as a
/// distance, which is what actually changes behaviour.
String targetLabel(DateTime? target) {
if (target == null) {
return "";
}
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
final DateTime day = DateTime(target.year, target.month, target.day);
final int days = day.difference(today).inDays;
if (days == 0) {
return "Due today";
}
if (days < 0) {
return "${-days} day${days == -1 ? "" : "s"} over";
}
return "$days day${days == 1 ? "" : "s"} left";
}
/// The painted landscape a routine kind wears on its cards.
List<Color> scenePalette(RoutineKind value) {
switch (value) {
case RoutineKind.Workout:
return sceneWorkout;
case RoutineKind.Meditation:
return sceneMeditation;
case RoutineKind.MustDo:
return sceneMustDo;
}
}
/// A landscape tinted from a goal's accent, so a goal's cards read as its own.
List<Color> goalScene(Color accent) {
final HSLColor base = HSLColor.fromColor(accent);
Color shade(double lightness, double saturation) => base
.withLightness(lightness)
.withSaturation((base.saturation * saturation).clamp(0.0, 1.0))
.toColor();
return <Color>[
shade(0.80, 0.55),
const Color(0xFFF6EFE2),
const Color(0xFFFFD9A0),
shade(0.66, 0.6),
shade(0.48, 0.8),
shade(0.30, 0.9),
const Color(0xFFE5793D),
];
}
/// The landscape for a task row, chosen by how much the task weighs.
List<Color> classScene(CommitmentClass value) {
switch (value) {
case CommitmentClass.NonNegotiable:
return sceneMustDo;
case CommitmentClass.Standard:
return sceneMeditation;
case CommitmentClass.Elective:
return sceneWorkout;
}
}
/// A stable seed from any string, so the same item always gets the same scene.
int sceneSeed(String key) {
int hash = 7;
for (int unit in key.codeUnits) {
hash = (hash * 31 + unit) & 0x7fffffff;
}
return hash;
}