feat(train): Home opens on this week, its totals and its seven days (ADR 0030, D60)
The week's time, distance and workouts above Home's list, and a strip of its days, each as tall as the time trained on it, starting on the day the person's language starts weeks on. Home's empty hint points to the Record tab, where its button had gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7473366f66
commit
064c44bfa5
11 changed files with 502 additions and 13 deletions
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
"aboutTitle": "Über Halkyon Train",
|
||||
|
||||
"homeRecordHint": "Zeichne eins mit dem Knopf unten auf.",
|
||||
"homeRecordHint": "Zeichne eins im Tab Aufzeichnen auf.",
|
||||
"recordAction": "Aufzeichnen",
|
||||
"recordBack": "Zurück zur Aufzeichnung",
|
||||
"recordSheetTitle": "Training aufzeichnen",
|
||||
|
|
@ -568,5 +568,10 @@
|
|||
"settingsTraining": "Training",
|
||||
"settingsVersion": "Version {version} (Build {build})",
|
||||
"recordingBarStopped": "Angehalten: speichern oder verwerfen",
|
||||
"recordingBarDistance": "{km} km"
|
||||
"recordingBarDistance": "{km} km",
|
||||
"homeWeekTitle": "Diese Woche",
|
||||
"homeWeekTime": "Zeit",
|
||||
"homeWeekDistance": "Distanz",
|
||||
"homeWeekWorkouts": "Trainings",
|
||||
"homeWeekDuration": "{hours, plural, =0{{minutes} Min.} other{{hours} Std. {minutes} Min.}}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
"aboutTitle": "About Halkyon Train",
|
||||
|
||||
"homeRecordHint": "Record one with the button below.",
|
||||
"homeRecordHint": "Record one from the Record tab.",
|
||||
"recordAction": "Record",
|
||||
"recordBack": "Back to the recording",
|
||||
"recordSheetTitle": "Record a workout",
|
||||
|
|
@ -717,5 +717,11 @@
|
|||
"@settingsVersion": {"placeholders": {"version": {"type": "String"}, "build": {"type": "String"}}},
|
||||
"recordingBarStopped": "Stopped: save or discard it",
|
||||
"recordingBarDistance": "{km} km",
|
||||
"@recordingBarDistance": {"placeholders": {"km": {"type": "String"}}}
|
||||
"@recordingBarDistance": {"placeholders": {"km": {"type": "String"}}},
|
||||
"homeWeekTitle": "This week",
|
||||
"homeWeekTime": "Time",
|
||||
"homeWeekDistance": "Distance",
|
||||
"homeWeekWorkouts": "Workouts",
|
||||
"homeWeekDuration": "{hours, plural, =0{{minutes} min} other{{hours} h {minutes} min}}",
|
||||
"@homeWeekDuration": {"placeholders": {"hours": {"type": "int"}, "minutes": {"type": "int"}}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @homeRecordHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Record one with the button below.'**
|
||||
/// **'Record one from the Record tab.'**
|
||||
String get homeRecordHint;
|
||||
|
||||
/// No description provided for @recordAction.
|
||||
|
|
@ -3435,6 +3435,36 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'{km} km'**
|
||||
String recordingBarDistance(String km);
|
||||
|
||||
/// No description provided for @homeWeekTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This week'**
|
||||
String get homeWeekTitle;
|
||||
|
||||
/// No description provided for @homeWeekTime.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Time'**
|
||||
String get homeWeekTime;
|
||||
|
||||
/// No description provided for @homeWeekDistance.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Distance'**
|
||||
String get homeWeekDistance;
|
||||
|
||||
/// No description provided for @homeWeekWorkouts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Workouts'**
|
||||
String get homeWeekWorkouts;
|
||||
|
||||
/// No description provided for @homeWeekDuration.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{hours, plural, =0{{minutes} min} other{{hours} h {minutes} min}}'**
|
||||
String homeWeekDuration(int hours, int minutes);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get aboutTitle => 'Über Halkyon Train';
|
||||
|
||||
@override
|
||||
String get homeRecordHint => 'Zeichne eins mit dem Knopf unten auf.';
|
||||
String get homeRecordHint => 'Zeichne eins im Tab Aufzeichnen auf.';
|
||||
|
||||
@override
|
||||
String get recordAction => 'Aufzeichnen';
|
||||
|
|
@ -2060,4 +2060,27 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String recordingBarDistance(String km) {
|
||||
return '$km km';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeWeekTitle => 'Diese Woche';
|
||||
|
||||
@override
|
||||
String get homeWeekTime => 'Zeit';
|
||||
|
||||
@override
|
||||
String get homeWeekDistance => 'Distanz';
|
||||
|
||||
@override
|
||||
String get homeWeekWorkouts => 'Trainings';
|
||||
|
||||
@override
|
||||
String homeWeekDuration(int hours, int minutes) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hours,
|
||||
locale: localeName,
|
||||
other: '$hours Std. $minutes Min.',
|
||||
zero: '$minutes Min.',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get aboutTitle => 'About Halkyon Train';
|
||||
|
||||
@override
|
||||
String get homeRecordHint => 'Record one with the button below.';
|
||||
String get homeRecordHint => 'Record one from the Record tab.';
|
||||
|
||||
@override
|
||||
String get recordAction => 'Record';
|
||||
|
|
@ -2048,6 +2048,29 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String recordingBarDistance(String km) {
|
||||
return '$km km';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeWeekTitle => 'This week';
|
||||
|
||||
@override
|
||||
String get homeWeekTime => 'Time';
|
||||
|
||||
@override
|
||||
String get homeWeekDistance => 'Distance';
|
||||
|
||||
@override
|
||||
String get homeWeekWorkouts => 'Workouts';
|
||||
|
||||
@override
|
||||
String homeWeekDuration(int hours, int minutes) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hours,
|
||||
locale: localeName,
|
||||
other: '$hours h $minutes min',
|
||||
zero: '$minutes min',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
|
||||
|
||||
/// The week [now] is in, summed up for Home (ADR 0030, D60), as Strava's:
|
||||
/// the workouts started in it, from the day the person's weeks start on
|
||||
/// ([firstDay], as [DateTime.weekday]), in local time. A workout counts its
|
||||
/// moving time, else its start to its end, on the day it started.
|
||||
WeekSummary weekSummary(
|
||||
Iterable<WorkoutContent> workouts, {
|
||||
required DateTime now,
|
||||
required int firstDay,
|
||||
}) {
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final back = (today.weekday - firstDay) % DateTime.daysPerWeek;
|
||||
// Days by the calendar, not by 24 hours, which a clock change breaks.
|
||||
final days = [
|
||||
for (var i = 0; i < DateTime.daysPerWeek; i++)
|
||||
DateTime(today.year, today.month, today.day - back + i),
|
||||
];
|
||||
final times = List.filled(DateTime.daysPerWeek, Duration.zero);
|
||||
var count = 0;
|
||||
var metres = 0;
|
||||
for (final workout in workouts) {
|
||||
final started = workout.startedAt.toLocal();
|
||||
final day = DateTime(started.year, started.month, started.day);
|
||||
final index = days.indexOf(day);
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
metres += workout.distanceMetres ?? 0;
|
||||
times[index] +=
|
||||
workout.movingTime ?? workout.endedAt.difference(workout.startedAt);
|
||||
}
|
||||
return WeekSummary._(
|
||||
start: days.first,
|
||||
days: [
|
||||
for (var i = 0; i < days.length; i++)
|
||||
(weekday: days[i].weekday, time: times[i]),
|
||||
],
|
||||
today: back,
|
||||
count: count,
|
||||
metres: metres,
|
||||
);
|
||||
}
|
||||
|
||||
/// One week's workouts, summed up.
|
||||
final class const WeekSummary._({
|
||||
/// The week's first day, at midnight, local time.
|
||||
required final DateTime start,
|
||||
|
||||
/// Its seven days in order, each its weekday and the time trained on it.
|
||||
required final List<({int weekday, Duration time})> days,
|
||||
|
||||
/// Which of [days] is today.
|
||||
required final int today,
|
||||
|
||||
/// How many workouts it holds, and how far they went.
|
||||
required final int count,
|
||||
required final int metres,
|
||||
}) with Equatable {
|
||||
/// The time trained in all.
|
||||
Duration get time => days.fold(Duration.zero, (sum, day) => sum + day.time);
|
||||
|
||||
/// The most trained in one of its days.
|
||||
Duration get longestDay => days.fold(
|
||||
Duration.zero,
|
||||
(longest, day) => day.time > longest ? day.time : longest,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [start, days, today, count, metres];
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/bloc/workouts_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/data/week_summary.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/widget/workout_texts.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// This week above Home's workouts (ADR 0030, D60), as Strava's: the time
|
||||
/// trained, the distance and the workouts, and a strip of its seven days,
|
||||
/// each as tall as the time trained on it, from the day the person's weeks
|
||||
/// start on in their language.
|
||||
class const WeekCard({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final theme = Theme.of(context);
|
||||
// Material's index counts from Sunday, 0; DateTime's from Monday, 1.
|
||||
final sunday0 = MaterialLocalizations.of(context).firstDayOfWeekIndex;
|
||||
final workouts = context.select<WorkoutsCubit, WeekSummary>(
|
||||
(cubit) => weekSummary(
|
||||
[
|
||||
for (final workout in cubit.state.visible) ?workout.content,
|
||||
],
|
||||
now: clock.now(),
|
||||
firstDay: sunday0 == 0 ? DateTime.sunday : sunday0,
|
||||
),
|
||||
);
|
||||
final time = workouts.time;
|
||||
return SectionCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.homeWeekTitle,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_Figure(
|
||||
label: l10n.homeWeekTime,
|
||||
value: l10n.homeWeekDuration(
|
||||
time.inHours,
|
||||
time.inMinutes.remainder(60),
|
||||
),
|
||||
),
|
||||
_Figure(
|
||||
label: l10n.homeWeekDistance,
|
||||
value: distanceText(l10n, locale, workouts.metres),
|
||||
),
|
||||
_Figure(
|
||||
label: l10n.homeWeekWorkouts,
|
||||
value: '${workouts.count}',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_DayStrip(workouts),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class const _Figure({required final String label, required final String value})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The week's days, a bar each as tall as the time trained on it against
|
||||
/// the longest, its weekday's letter below, today's marked.
|
||||
class const _DayStrip(final WeekSummary week) extends StatelessWidget {
|
||||
static const double _height = 40;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final letter = DateFormat.EEEEE(
|
||||
Localizations.localeOf(context).toString(),
|
||||
);
|
||||
final longest = week.longestDay.inSeconds;
|
||||
return Row(
|
||||
children: [
|
||||
for (final (index, day) in week.days.indexed)
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _height,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: longest == 0 || day.time == Duration.zero
|
||||
? 4
|
||||
: 4 + (_height - 4) * day.time.inSeconds / longest,
|
||||
decoration: BoxDecoration(
|
||||
color: day.time == Duration.zero
|
||||
? scheme.surfaceContainerHighest
|
||||
: scheme.primary,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
letter.format(
|
||||
week.start.add(Duration(days: index, hours: 12)),
|
||||
),
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
fontWeight: index == week.today ? FontWeight.w700 : null,
|
||||
color: index == week.today
|
||||
? scheme.primary
|
||||
: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,9 +12,12 @@ import 'package:material_ui/material_ui.dart';
|
|||
|
||||
/// The person's workouts as home lists them (ADR 0030, D24): newest first
|
||||
/// under the month each started in, those this device can't open last;
|
||||
/// [empty] while there are none.
|
||||
class const WorkoutsList({required final Widget empty, super.key})
|
||||
extends StatelessWidget {
|
||||
/// [empty] while there are none; [header], where given, above them.
|
||||
class const WorkoutsList({
|
||||
required final Widget empty,
|
||||
final Widget? header,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
|
|
@ -54,6 +57,7 @@ class const WorkoutsList({required final Widget empty, super.key})
|
|||
// Room for the button to record over the last row.
|
||||
padding: const EdgeInsets.only(bottom: 88),
|
||||
children: [
|
||||
?header,
|
||||
for (final row in rows)
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/widget/week_card.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/widget/workouts_list.dart';
|
||||
import 'package:halkyon_train_flutter/src/settings/settings_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/tab_screen.dart';
|
||||
|
|
@ -8,7 +9,8 @@ import 'package:halkyon_ui/halkyon_ui.dart';
|
|||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Home (ADR 0030, D60): the person's workouts (D24), a reset of their keys
|
||||
/// Home (ADR 0030, D60): this week ([WeekCard]) over the person's workouts
|
||||
/// (D24), a reset of their keys
|
||||
/// waiting above them (`KeysResetBanner`); the cog to Settings.
|
||||
class const HomeTab({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
|
|
@ -23,6 +25,17 @@ class const HomeTab({super.key}) extends StatelessWidget {
|
|||
const KeysResetBanner(),
|
||||
Expanded(
|
||||
child: WorkoutsList(
|
||||
header: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: readableContentMaxWidth,
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(12, 8, 12, 0),
|
||||
child: WeekCard(),
|
||||
),
|
||||
),
|
||||
),
|
||||
empty: Center(
|
||||
child: EmptyListMessage(
|
||||
icon: LucideIcons.activity,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import 'package:halkyon_train_flutter/src/feature/route/data/routing_gateway.dar
|
|||
import 'package:halkyon_train_flutter/src/feature/route/model/route.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/data/workout_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/widget/week_card.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
|
|
@ -2899,7 +2900,10 @@ void main() {
|
|||
expect(top('Morning run'), lessThan(top('September 2026')));
|
||||
expect(top('September 2026'), lessThan(top('Lakeside')));
|
||||
expect(top('Lakeside'), lessThan(top('Locked workout')));
|
||||
expect(find.text('2.50 km'), findsNWidgets(2));
|
||||
// In the list, not in the week above it.
|
||||
Finder listed(String text) =>
|
||||
find.descendant(of: find.byType(ListTile), matching: find.text(text));
|
||||
expect(listed('2.50 km'), findsNWidgets(2));
|
||||
expect(find.text('12:30'), findsNWidgets(2));
|
||||
expect(find.text('5:00 /km'), findsNWidgets(2));
|
||||
expect(find.text('30:00'), findsOneWidget);
|
||||
|
|
@ -2909,6 +2913,42 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets('above them, this week: its time, distance and workouts, and '
|
||||
'its days from Monday in British English (D60)', (tester) async {
|
||||
tester.platformDispatcher.localesTestValue = const [Locale('en', 'GB')];
|
||||
addTearDown(tester.platformDispatcher.clearLocalesTestValue);
|
||||
// Saturday, 31 October 2026.
|
||||
await withClock(Clock.fixed(DateTime(2026, 10, 31, 15)), () async {
|
||||
await saveRun(DateTime(2026, 10, 27, 7, 30));
|
||||
await saveRow();
|
||||
// Sunday before: last week's.
|
||||
await saveRun(DateTime(2026, 10, 25, 9));
|
||||
await startWith(tester);
|
||||
|
||||
final week = find.byType(WeekCard);
|
||||
Finder inWeek(String text) =>
|
||||
find.descendant(of: week, matching: find.text(text));
|
||||
expect(inWeek('This week'), findsOneWidget);
|
||||
// 12:30 running and 30:00 rowing.
|
||||
expect(inWeek('42 min'), findsOneWidget);
|
||||
expect(inWeek('2.50 km'), findsOneWidget);
|
||||
expect(inWeek('2'), findsOneWidget);
|
||||
final letters = tester
|
||||
.widgetList<Text>(
|
||||
find.descendant(of: week, matching: find.byType(Text)),
|
||||
)
|
||||
.map((text) => text.data)
|
||||
.toList();
|
||||
expect(letters.sublist(letters.length - 7), [
|
||||
'M', 'T', 'W', 'T', 'F', 'S', 'S', //
|
||||
]);
|
||||
expect(
|
||||
tester.getTopLeft(week).dy,
|
||||
lessThan(tester.getTopLeft(find.text('October 2026')).dy),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets("a workout's page shows its map, totals, splits and charts, "
|
||||
'and where the map comes from', (tester) async {
|
||||
await saveRun(DateTime(2026, 10, 27, 7, 30));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/data/week_summary.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
|
||||
|
||||
/// The week Home sums up above the workouts (ADR 0030, D60): from the day
|
||||
/// the person's weeks start on, in their own time, each workout counted on
|
||||
/// the day it started.
|
||||
void main() {
|
||||
// Saturday, 31 October 2026, in the afternoon, local time.
|
||||
final now = DateTime(2026, 10, 31, 15);
|
||||
|
||||
WorkoutContent workout(
|
||||
DateTime start, {
|
||||
Duration length = const Duration(hours: 1),
|
||||
Duration? moving,
|
||||
int? metres,
|
||||
}) => WorkoutContent(
|
||||
activity: Activity.running,
|
||||
startedAt: start.toUtc(),
|
||||
endedAt: start.add(length).toUtc(),
|
||||
movingTime: moving,
|
||||
distanceMetres: metres,
|
||||
);
|
||||
|
||||
test('a week starting on Monday sums the workouts from Monday on', () {
|
||||
final week = weekSummary(
|
||||
[
|
||||
workout(
|
||||
DateTime(2026, 10, 26, 7),
|
||||
moving: const Duration(minutes: 40),
|
||||
metres: 8000,
|
||||
),
|
||||
workout(
|
||||
DateTime(2026, 10, 31, 9),
|
||||
moving: const Duration(minutes: 20),
|
||||
metres: 4500,
|
||||
),
|
||||
// Sunday before: last week.
|
||||
workout(DateTime(2026, 10, 25, 23, 30), metres: 10000),
|
||||
],
|
||||
now: now,
|
||||
firstDay: DateTime.monday,
|
||||
);
|
||||
|
||||
expect(week.start, DateTime(2026, 10, 26));
|
||||
expect(week.count, 2);
|
||||
expect(week.time, const Duration(hours: 1));
|
||||
expect(week.metres, 12500);
|
||||
expect(week.days.map((d) => d.weekday), [1, 2, 3, 4, 5, 6, 7]);
|
||||
expect(week.days.map((d) => d.time.inMinutes), [40, 0, 0, 0, 0, 20, 0]);
|
||||
expect(week.today, 5);
|
||||
});
|
||||
|
||||
test('a week starting on Sunday takes in the Sunday before', () {
|
||||
final week = weekSummary(
|
||||
[
|
||||
workout(DateTime(2026, 10, 25, 23, 30)),
|
||||
],
|
||||
now: now,
|
||||
firstDay: DateTime.sunday,
|
||||
);
|
||||
|
||||
expect(week.start, DateTime(2026, 10, 25));
|
||||
expect(week.days.first.weekday, DateTime.sunday);
|
||||
expect(week.days.first.time, const Duration(hours: 1));
|
||||
expect(week.today, 6);
|
||||
});
|
||||
|
||||
test('without its moving time a workout counts from start to end; '
|
||||
'without a distance, none', () {
|
||||
final week = weekSummary(
|
||||
[
|
||||
workout(
|
||||
DateTime(2026, 10, 27, 18),
|
||||
length: const Duration(minutes: 45),
|
||||
),
|
||||
],
|
||||
now: now,
|
||||
firstDay: DateTime.monday,
|
||||
);
|
||||
|
||||
expect(week.time, const Duration(minutes: 45));
|
||||
expect(week.metres, 0);
|
||||
expect(week.count, 1);
|
||||
});
|
||||
|
||||
test('a week without workouts is all naught', () {
|
||||
final week = weekSummary(const [], now: now, firstDay: DateTime.monday);
|
||||
|
||||
expect(week.count, 0);
|
||||
expect(week.time, Duration.zero);
|
||||
expect(week.days, hasLength(7));
|
||||
expect(week.longestDay, Duration.zero);
|
||||
});
|
||||
|
||||
test('the longest day scales the strip', () {
|
||||
final week = weekSummary(
|
||||
[
|
||||
workout(DateTime(2026, 10, 26, 7), moving: const Duration(minutes: 30)),
|
||||
workout(
|
||||
DateTime(2026, 10, 26, 18),
|
||||
moving: const Duration(minutes: 30),
|
||||
),
|
||||
workout(DateTime(2026, 10, 28, 7), moving: const Duration(minutes: 20)),
|
||||
],
|
||||
now: now,
|
||||
firstDay: DateTime.monday,
|
||||
);
|
||||
|
||||
expect(week.longestDay, const Duration(hours: 1));
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue