feat(statistiques): implémentation frontend statistiques de progression (ticket #82)
Lots F1+F2 : écran progression_screen.dart, intégration home/history/navigation, tests associés (193 tests suite complète, 4 échecs préexistants sans rapport confirmés). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
602
test/presentation/progression_screen_test.dart
Normal file
602
test/presentation/progression_screen_test.dart
Normal file
@ -0,0 +1,602 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gametime/application/application.dart';
|
||||
import 'package:gametime/domain/domain.dart';
|
||||
import 'package:gametime/presentation/presentation.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Progression affiche la période par défaut et l’état vide global', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = _FakeProgressionStatsRepository();
|
||||
|
||||
await tester.pumpWidget(_app(repository: repository));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Progression'), findsOneWidget);
|
||||
expect(find.text('4 semaines'), findsOneWidget);
|
||||
expect(find.text('3 mois'), findsOneWidget);
|
||||
expect(find.text('Tout'), findsOneWidget);
|
||||
expect(find.text('Aucune progression à afficher'), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
'Termine une séance pour voir ton volume, ton rythme et tes résultats évoluer ici.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Voir les séances'), findsOneWidget);
|
||||
expect(repository.periods, [ProgressionPeriod.fourWeeks]);
|
||||
});
|
||||
|
||||
testWidgets('Progression affiche Volume et Régularité', (tester) async {
|
||||
final repository = _FakeProgressionStatsRepository()
|
||||
..overview = ProgressionGlobalStatsData(
|
||||
completedSessionCount: 8,
|
||||
totalActiveMs: 20400000,
|
||||
activeWeekStarts: [
|
||||
DateTime.utc(2026, 7, 6),
|
||||
DateTime.utc(2026, 7, 13),
|
||||
DateTime.utc(2026, 7, 20),
|
||||
],
|
||||
hasAnyCompletedHistory: true,
|
||||
firstCompletedSessionAt: DateTime.utc(2026, 7, 1),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_app(repository: repository));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Volume'), findsOneWidget);
|
||||
expect(find.text('Séances'), findsOneWidget);
|
||||
expect(find.text('8'), findsOneWidget);
|
||||
expect(find.text('Temps total'), findsOneWidget);
|
||||
expect(find.text('5 h 40'), findsOneWidget);
|
||||
expect(find.text('Régularité'), findsOneWidget);
|
||||
expect(find.text('Rythme récent'), findsOneWidget);
|
||||
expect(find.text('3 / 4 semaines actives'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Une semaine active contient au moins une séance terminée.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Par exercice affiche un exercice archivé et un point unique', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = _FakeProgressionStatsRepository()
|
||||
..overview = ProgressionGlobalStatsData(
|
||||
completedSessionCount: 1,
|
||||
totalActiveMs: 1800000,
|
||||
activeWeekStarts: [DateTime.utc(2026, 7, 13)],
|
||||
hasAnyCompletedHistory: true,
|
||||
)
|
||||
..exerciseOptions = [
|
||||
ProgressionExerciseOptionData(
|
||||
exerciseKey: 'shoot',
|
||||
nameSnapshot: 'Shoot ligne',
|
||||
isArchived: true,
|
||||
lastPerformedAt: DateTime.utc(2026, 7, 17),
|
||||
),
|
||||
]
|
||||
..measuresByExercise = const {
|
||||
'shoot': [
|
||||
ProgressionMeasureOptionData(
|
||||
measure: ProgressionMeasure.reps,
|
||||
label: 'Répétitions',
|
||||
lowerIsBetter: false,
|
||||
),
|
||||
],
|
||||
}
|
||||
..seriesByExerciseMeasure = {
|
||||
'shoot:${ProgressionMeasure.reps.name}': ProgressionExerciseSeriesData(
|
||||
exerciseKey: 'shoot',
|
||||
exerciseNameSnapshot: 'Shoot ligne',
|
||||
measure: ProgressionMeasureOptionData(
|
||||
measure: ProgressionMeasure.reps,
|
||||
label: 'Répétitions',
|
||||
lowerIsBetter: false,
|
||||
),
|
||||
points: [
|
||||
ProgressionPointData(
|
||||
workoutHistoryId: 'history-1',
|
||||
workoutNameSnapshot: 'Prépa match',
|
||||
startedAt: DateTime.utc(2026, 7, 17),
|
||||
value: 45,
|
||||
rawValue: 45,
|
||||
),
|
||||
],
|
||||
hasAnyAllTimeData: true,
|
||||
hasAnyCompletedExerciseResult: true,
|
||||
),
|
||||
};
|
||||
|
||||
await tester.pumpWidget(_app(repository: repository));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Par exercice'), findsOneWidget);
|
||||
expect(find.text('Choisir un exercice'), findsOneWidget);
|
||||
expect(find.text('Shoot ligne'), findsOneWidget);
|
||||
expect(find.text('Exercice archivé'), findsOneWidget);
|
||||
expect(find.text('Encore un point de départ'), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
'Termine cet exercice dans une autre séance pour voir son évolution.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Dernière séance : 45 répétitions'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Par exercice affiche Temps réalisé et Plus bas = mieux', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = _progressionRepositoryWithSeries(
|
||||
measure: const ProgressionMeasureOptionData(
|
||||
measure: ProgressionMeasure.stopwatchScore,
|
||||
label: 'Temps réalisé',
|
||||
lowerIsBetter: true,
|
||||
),
|
||||
rawValues: [45100, 42800],
|
||||
values: [45100, 42800],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_app(repository: repository));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Temps réalisé'), findsOneWidget);
|
||||
expect(find.text('Plus bas = mieux'), findsOneWidget);
|
||||
expect(find.text('Meilleur : 00:42.8'), findsOneWidget);
|
||||
expect(find.text('Dernier : 00:42.8'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tap sur un point affiche le détail et ouvre la séance', (
|
||||
tester,
|
||||
) async {
|
||||
final historyRepository = _FakeWorkoutHistoryRepository()
|
||||
..histories['history-2'] = _history(id: 'history-2');
|
||||
final repository = _progressionRepositoryWithSeries(
|
||||
measure: const ProgressionMeasureOptionData(
|
||||
measure: ProgressionMeasure.manualScore,
|
||||
label: 'Score',
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
lowerIsBetter: false,
|
||||
),
|
||||
rawValues: [12.0, 14.0],
|
||||
values: [12, 14],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_app(repository: repository, historyRepository: historyRepository),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final pointFinder = find.byKey(const ValueKey('progression-point-1'));
|
||||
await tester.ensureVisible(pointFinder);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(pointFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('12 juillet'), findsOneWidget);
|
||||
expect(find.text('Séance : Prépa match'), findsOneWidget);
|
||||
expect(find.text('Meilleur score : 14 paniers'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Voir la séance'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(historyRepository.requestedIds, ['history-2']);
|
||||
expect(find.text('Séance jambes'), findsWidgets);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _app({
|
||||
required _FakeProgressionStatsRepository repository,
|
||||
_FakeWorkoutHistoryRepository? historyRepository,
|
||||
}) {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 22, 12));
|
||||
final activeRepository = _FakeActiveSessionRepository();
|
||||
final resolvedHistoryRepository =
|
||||
historyRepository ?? _FakeWorkoutHistoryRepository();
|
||||
return MaterialApp(
|
||||
theme: courtBlazerLightTheme(),
|
||||
home: ProgressionScreen(
|
||||
progressionStatsUseCase: ProgressionStatsUseCase(
|
||||
repository: repository,
|
||||
clock: clock,
|
||||
),
|
||||
historyUseCases: WorkoutHistoryUseCases(
|
||||
repository: resolvedHistoryRepository,
|
||||
clock: clock,
|
||||
),
|
||||
workoutTemplateUseCases: WorkoutTemplateUseCases(
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
programRepository: _FakeProgramRepository(),
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
activeUseCases: ActiveWorkoutSessionUseCases(
|
||||
sessionRepository: activeRepository,
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
closeUseCase: CloseWorkoutSessionUseCase(
|
||||
sessionRepository: activeRepository,
|
||||
historyRepository: resolvedHistoryRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_FakeProgressionStatsRepository _progressionRepositoryWithSeries({
|
||||
required ProgressionMeasureOptionData measure,
|
||||
required List<Object> rawValues,
|
||||
required List<double> values,
|
||||
}) {
|
||||
return _FakeProgressionStatsRepository()
|
||||
..overview = ProgressionGlobalStatsData(
|
||||
completedSessionCount: 2,
|
||||
totalActiveMs: 3600000,
|
||||
activeWeekStarts: [DateTime.utc(2026, 7, 6), DateTime.utc(2026, 7, 13)],
|
||||
hasAnyCompletedHistory: true,
|
||||
)
|
||||
..exerciseOptions = [
|
||||
ProgressionExerciseOptionData(
|
||||
exerciseKey: 'shoot',
|
||||
nameSnapshot: 'Shoot ligne',
|
||||
isArchived: false,
|
||||
lastPerformedAt: DateTime.utc(2026, 7, 17),
|
||||
),
|
||||
]
|
||||
..measuresByExercise = {
|
||||
'shoot': [measure],
|
||||
}
|
||||
..seriesByExerciseMeasure = {
|
||||
'shoot:${measure.measure.name}': ProgressionExerciseSeriesData(
|
||||
exerciseKey: 'shoot',
|
||||
exerciseNameSnapshot: 'Shoot ligne',
|
||||
measure: measure,
|
||||
points: [
|
||||
ProgressionPointData(
|
||||
workoutHistoryId: 'history-1',
|
||||
workoutNameSnapshot: 'Prépa match',
|
||||
startedAt: DateTime.utc(2026, 7, 5),
|
||||
value: values[0],
|
||||
rawValue: rawValues[0],
|
||||
),
|
||||
ProgressionPointData(
|
||||
workoutHistoryId: 'history-2',
|
||||
workoutNameSnapshot: 'Prépa match',
|
||||
startedAt: DateTime.utc(2026, 7, 12),
|
||||
value: values[1],
|
||||
rawValue: rawValues[1],
|
||||
),
|
||||
],
|
||||
hasAnyAllTimeData: true,
|
||||
hasAnyCompletedExerciseResult: true,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
final class _FakeProgressionStatsRepository
|
||||
implements ProgressionStatsRepository {
|
||||
ProgressionGlobalStatsData overview = const ProgressionGlobalStatsData(
|
||||
completedSessionCount: 0,
|
||||
totalActiveMs: 0,
|
||||
activeWeekStarts: [],
|
||||
hasAnyCompletedHistory: false,
|
||||
);
|
||||
List<ProgressionExerciseOptionData> exerciseOptions = const [];
|
||||
Map<String, List<ProgressionMeasureOptionData>> measuresByExercise = const {};
|
||||
Map<String, ProgressionExerciseSeriesData> seriesByExerciseMeasure = const {};
|
||||
final periods = <ProgressionPeriod>[];
|
||||
|
||||
@override
|
||||
Future<ProgressionGlobalStatsData> readGlobalStats(
|
||||
ProgressionDateRange range,
|
||||
) async {
|
||||
periods.add(
|
||||
range.startedAt == null
|
||||
? ProgressionPeriod.all
|
||||
: range.startedAt!.isAfter(DateTime.utc(2026, 5))
|
||||
? ProgressionPeriod.fourWeeks
|
||||
: ProgressionPeriod.threeMonths,
|
||||
);
|
||||
return overview;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ProgressionExerciseOptionData>> listExerciseOptions(
|
||||
ProgressionDateRange range,
|
||||
) async {
|
||||
return exerciseOptions;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ProgressionMeasureOptionData>> listMeasureOptions({
|
||||
required ProgressionDateRange range,
|
||||
required String exerciseKey,
|
||||
}) async {
|
||||
return measuresByExercise[exerciseKey] ?? const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ProgressionExerciseSeriesData> readExerciseSeries({
|
||||
required ProgressionDateRange range,
|
||||
required String exerciseKey,
|
||||
required ProgressionMeasure measure,
|
||||
}) async {
|
||||
return seriesByExerciseMeasure['$exerciseKey:${measure.name}'] ??
|
||||
ProgressionExerciseSeriesData(
|
||||
exerciseKey: exerciseKey,
|
||||
exerciseNameSnapshot: exerciseKey,
|
||||
measure: ProgressionMeasureOptionData(
|
||||
measure: measure,
|
||||
label: 'Score',
|
||||
lowerIsBetter: false,
|
||||
),
|
||||
points: const [],
|
||||
hasAnyAllTimeData: false,
|
||||
hasAnyCompletedExerciseResult: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WorkoutHistory _history({required String id}) {
|
||||
final start = DateTime.utc(2026, 7, 12, 10);
|
||||
return WorkoutHistory(
|
||||
metadata: _metadata(id),
|
||||
sourceWorkoutTemplateId: 'template-1',
|
||||
sourceActiveWorkoutSessionId: 'session-1',
|
||||
nameSnapshot: 'Séance jambes',
|
||||
startedAt: start,
|
||||
endedAt: start.add(const Duration(minutes: 30)),
|
||||
totalActiveMs: 1800000,
|
||||
completed: true,
|
||||
historySnapshotJson: jsonEncode({
|
||||
'sessionId': 'session-1',
|
||||
'resolvedTemplateSnapshotJson': jsonEncode({
|
||||
'name': 'Séance jambes',
|
||||
'programs': [
|
||||
{
|
||||
'id': 'program-snapshot-1',
|
||||
'programNameSnapshot': 'Programme jambes',
|
||||
'programSnapshotJson': jsonEncode({
|
||||
'exercises': [
|
||||
{
|
||||
'id': 'exercise-snapshot-1',
|
||||
'exerciseNameSnapshot': 'Squat',
|
||||
'setsCount': 1,
|
||||
'timeEnabled': true,
|
||||
'repsEnabled': true,
|
||||
'scoreEnabled': true,
|
||||
'targetTimeSeconds': 45,
|
||||
'targetReps': 10,
|
||||
'targetScore': 80,
|
||||
'scoreInputModeSnapshot': ScoreInputMode.manual.name,
|
||||
'scoreLabelSnapshot': 'Charge',
|
||||
'scoreUnitSnapshot': 'kg',
|
||||
'restSecondsOverride': 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
'overrides': const [],
|
||||
}),
|
||||
'results': [
|
||||
{
|
||||
'programSnapshotId': 'program-snapshot-1',
|
||||
'exerciseSnapshotId': 'exercise-snapshot-1',
|
||||
'programIndex': 0,
|
||||
'exerciseIndex': 0,
|
||||
'setIndex': 0,
|
||||
'actualTimeMs': 45000,
|
||||
'actualReps': 10,
|
||||
'actualScore': 80,
|
||||
'scoreLabelSnapshot': 'Charge',
|
||||
'scoreInputModeSnapshot': ScoreInputMode.manual.name,
|
||||
'scoreUnitSnapshot': 'kg',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
EntityMetadata _metadata(String id) {
|
||||
return EntityMetadata(
|
||||
id: id,
|
||||
createdAt: DateTime.utc(2026, 7, 17),
|
||||
updatedAt: DateTime.utc(2026, 7, 17),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
final class _FakeClock implements Clock {
|
||||
_FakeClock(this.value);
|
||||
|
||||
final DateTime value;
|
||||
|
||||
@override
|
||||
DateTime now() => value;
|
||||
}
|
||||
|
||||
final class _FakeIds implements IdGenerator {
|
||||
var _next = 0;
|
||||
|
||||
@override
|
||||
String newId() {
|
||||
_next += 1;
|
||||
return 'id-$_next';
|
||||
}
|
||||
}
|
||||
|
||||
final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
final histories = <String, WorkoutHistory>{};
|
||||
final requestedIds = <String>[];
|
||||
|
||||
@override
|
||||
Future<void> delete(String id, DateTime deletedAt) async {}
|
||||
|
||||
@override
|
||||
Future<WorkoutHistory?> findById(String id) async {
|
||||
requestedIds.add(id);
|
||||
return histories[id];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<WorkoutHistory>> listActive() async => histories.values.toList();
|
||||
|
||||
@override
|
||||
Future<void> save(WorkoutHistory history) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
|
||||
}
|
||||
|
||||
final class _FakeWorkoutTemplateRepository
|
||||
implements WorkoutTemplateRepository {
|
||||
@override
|
||||
Future<WorkoutTemplate?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<WorkoutTemplate>> listActive() async => const [];
|
||||
|
||||
@override
|
||||
Future<void> replaceComposition(
|
||||
WorkoutTemplate template,
|
||||
DateTime deletedAt,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<void> save(WorkoutTemplate template) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveProgram(WorkoutTemplateProgram program) async {}
|
||||
}
|
||||
|
||||
final class _FakeProgramRepository implements ProgramRepository {
|
||||
@override
|
||||
Future<Program?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<Program>> listActive() async => const [];
|
||||
|
||||
@override
|
||||
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
|
||||
|
||||
@override
|
||||
Future<void> save(Program program) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||||
}
|
||||
|
||||
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
@override
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required DateTime deletedAt,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findOpen() async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveRestState?> findRestStateById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveSetTimerState?> findSetTimerState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async => null;
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||||
String sessionId,
|
||||
) async => const [];
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||||
String sessionId,
|
||||
) async => const [];
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||
String sessionId,
|
||||
) async => const [];
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetTimerState>> listSetTimerStates(String sessionId) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepProgressState(
|
||||
ActiveExerciseStepProgressState state,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveRestState(ActiveRestState restState) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetTimerState(ActiveSetTimerState state) async {}
|
||||
}
|
||||
Reference in New Issue
Block a user