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 names = [ "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 initials = ["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 days) { if (days.isEmpty) { return "On demand"; } final List sorted = List.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 parts = []; 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"; }