feat(ui): écran Historique des séances
Ajoute l'écran d'historique des séances (presentation/history_screen.dart), avec les ajustements application/domaine/repositories nécessaires et la navigation depuis l'accueil. Complète le dernier écran du périmètre UI. flutter analyze propre, 18/18 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -10,6 +10,7 @@ final class AppBootstrap {
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.activeWorkoutSessionUseCases,
|
||||
required this.closeWorkoutSessionUseCase,
|
||||
required this.workoutHistoryUseCases,
|
||||
});
|
||||
|
||||
final AppDatabase database;
|
||||
@ -19,6 +20,7 @@ final class AppBootstrap {
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
|
||||
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
||||
final WorkoutHistoryUseCases workoutHistoryUseCases;
|
||||
|
||||
static Future<AppBootstrap> create() async {
|
||||
final database = AppDatabase.open();
|
||||
@ -77,6 +79,10 @@ final class AppBootstrap {
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
),
|
||||
workoutHistoryUseCases: WorkoutHistoryUseCases(
|
||||
repository: historyRepository,
|
||||
clock: clock,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -85,6 +85,9 @@ abstract interface class ActiveSessionRepository {
|
||||
}
|
||||
|
||||
abstract interface class WorkoutHistoryRepository {
|
||||
Future<WorkoutHistory?> findById(String id);
|
||||
Future<List<WorkoutHistory>> listActive();
|
||||
Future<void> save(WorkoutHistory history);
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult result);
|
||||
Future<void> delete(String id, DateTime deletedAt);
|
||||
}
|
||||
|
||||
@ -638,6 +638,28 @@ final class ActiveWorkoutSessionUseCases {
|
||||
return session;
|
||||
}
|
||||
|
||||
Future<ActiveWorkoutSession> startFromHistory(WorkoutHistory history) async {
|
||||
final snapshot =
|
||||
jsonDecode(history.historySnapshotJson) as Map<String, dynamic>;
|
||||
final resolvedTemplateSnapshotJson =
|
||||
snapshot['resolvedTemplateSnapshotJson'] as String;
|
||||
final now = clock.now();
|
||||
final session = ActiveWorkoutSession(
|
||||
metadata: _newMetadata(ids, originDeviceId, now),
|
||||
sourceWorkoutTemplateId: null,
|
||||
status: ActiveWorkoutStatus.running,
|
||||
startedAt: now,
|
||||
lastPersistedAt: now,
|
||||
elapsedActiveMs: 0,
|
||||
currentProgramIndex: 0,
|
||||
currentExerciseIndex: 0,
|
||||
currentSetIndex: 0,
|
||||
resolvedTemplateSnapshotJson: resolvedTemplateSnapshotJson,
|
||||
);
|
||||
await sessionRepository.save(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
Future<ActiveWorkoutSession> pause(String sessionId) async {
|
||||
final session = await _requiredSession(sessionId);
|
||||
final paused = session.pause(clock.now());
|
||||
@ -851,6 +873,19 @@ final class CloseWorkoutSessionUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkoutHistoryUseCases {
|
||||
const WorkoutHistoryUseCases({required this.repository, required this.clock});
|
||||
|
||||
final WorkoutHistoryRepository repository;
|
||||
final Clock clock;
|
||||
|
||||
Future<WorkoutHistory?> findById(String id) => repository.findById(id);
|
||||
|
||||
Future<List<WorkoutHistory>> listActive() => repository.listActive();
|
||||
|
||||
Future<void> delete(String id) => repository.delete(id, clock.now());
|
||||
}
|
||||
|
||||
EntityMetadata _newMetadata(
|
||||
IdGenerator ids,
|
||||
String originDeviceId,
|
||||
|
||||
@ -428,6 +428,52 @@ final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
|
||||
final db.AppDatabase database;
|
||||
|
||||
@override
|
||||
Future<domain.WorkoutHistory?> findById(String id) async {
|
||||
final row =
|
||||
await (database.select(
|
||||
database.workoutHistories,
|
||||
)..where((table) => table.id.equals(id) & table.deletedAt.isNull()))
|
||||
.getSingleOrNull();
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
final results =
|
||||
await (database.select(database.workoutHistorySetResults)
|
||||
..where(
|
||||
(table) =>
|
||||
table.workoutHistoryId.equals(id) &
|
||||
table.deletedAt.isNull(),
|
||||
)
|
||||
..orderBy([
|
||||
(table) => OrderingTerm.asc(table.programIndex),
|
||||
(table) => OrderingTerm.asc(table.exerciseIndex),
|
||||
(table) => OrderingTerm.asc(table.setIndex),
|
||||
]))
|
||||
.get();
|
||||
return _workoutHistoryFromRow(
|
||||
row,
|
||||
results.map(_workoutHistorySetResultFromRow).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<domain.WorkoutHistory>> listActive() async {
|
||||
final rows =
|
||||
await (database.select(database.workoutHistories)
|
||||
..where((table) => table.deletedAt.isNull())
|
||||
..orderBy([(table) => OrderingTerm.desc(table.startedAt)]))
|
||||
.get();
|
||||
final histories = <domain.WorkoutHistory>[];
|
||||
for (final row in rows) {
|
||||
final history = await findById(row.id);
|
||||
if (history != null) {
|
||||
histories.add(history);
|
||||
}
|
||||
}
|
||||
return histories;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(domain.WorkoutHistory history) async {
|
||||
await database
|
||||
@ -444,6 +490,28 @@ final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
.into(database.workoutHistorySetResults)
|
||||
.insertOnConflictUpdate(_workoutHistorySetResultCompanion(result));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id, DateTime deletedAt) async {
|
||||
await database.transaction(() async {
|
||||
await (database.update(
|
||||
database.workoutHistories,
|
||||
)..where((table) => table.id.equals(id))).write(
|
||||
db.WorkoutHistoriesCompanion(
|
||||
deletedAt: Value(deletedAt),
|
||||
updatedAt: Value(deletedAt),
|
||||
),
|
||||
);
|
||||
await (database.update(
|
||||
database.workoutHistorySetResults,
|
||||
)..where((table) => table.workoutHistoryId.equals(id))).write(
|
||||
db.WorkoutHistorySetResultsCompanion(
|
||||
deletedAt: Value(deletedAt),
|
||||
updatedAt: Value(deletedAt),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
domain.EntityMetadata _metadataFromRow(dynamic row) {
|
||||
@ -986,6 +1054,53 @@ db.WorkoutHistorySetResultsCompanion _workoutHistorySetResultCompanion(
|
||||
);
|
||||
}
|
||||
|
||||
domain.WorkoutHistory _workoutHistoryFromRow(
|
||||
db.WorkoutHistory row,
|
||||
List<domain.WorkoutHistorySetResult> results,
|
||||
) {
|
||||
return domain.WorkoutHistory(
|
||||
metadata: _metadataFromRow(row),
|
||||
sourceWorkoutTemplateId: row.sourceWorkoutTemplateId,
|
||||
sourceActiveWorkoutSessionId: row.sourceActiveWorkoutSessionId,
|
||||
nameSnapshot: row.nameSnapshot,
|
||||
startedAt: row.startedAt,
|
||||
endedAt: row.endedAt,
|
||||
totalActiveMs: row.totalActiveMs,
|
||||
completed: row.completed,
|
||||
historySnapshotJson: row.historySnapshotJson,
|
||||
results: results,
|
||||
);
|
||||
}
|
||||
|
||||
domain.WorkoutHistorySetResult _workoutHistorySetResultFromRow(
|
||||
db.WorkoutHistorySetResult row,
|
||||
) {
|
||||
return domain.WorkoutHistorySetResult(
|
||||
metadata: _metadataFromRow(row),
|
||||
workoutHistoryId: row.workoutHistoryId,
|
||||
programSnapshotId: row.programSnapshotId,
|
||||
exerciseSnapshotId: row.exerciseSnapshotId,
|
||||
programIndex: row.programIndex,
|
||||
exerciseIndex: row.exerciseIndex,
|
||||
setIndex: row.setIndex,
|
||||
programNameSnapshot: row.programNameSnapshot,
|
||||
exerciseNameSnapshot: row.exerciseNameSnapshot,
|
||||
timeEnabledSnapshot: row.timeEnabledSnapshot,
|
||||
repsEnabledSnapshot: row.repsEnabledSnapshot,
|
||||
scoreEnabledSnapshot: row.scoreEnabledSnapshot,
|
||||
targetTimeSecondsSnapshot: row.targetTimeSecondsSnapshot,
|
||||
targetRepsSnapshot: row.targetRepsSnapshot,
|
||||
targetScoreSnapshot: row.targetScoreSnapshot,
|
||||
actualTimeMs: row.actualTimeMs,
|
||||
actualReps: row.actualReps,
|
||||
actualScore: row.actualScore,
|
||||
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
||||
startedAt: row.startedAt,
|
||||
completedAt: row.completedAt,
|
||||
);
|
||||
}
|
||||
|
||||
String _syncStateToDb(domain.SyncState state) => switch (state) {
|
||||
domain.SyncState.localOnly => 'localOnly',
|
||||
domain.SyncState.dirty => 'dirty',
|
||||
|
||||
430
lib/presentation/history_screen.dart
Normal file
430
lib/presentation/history_screen.dart
Normal file
@ -0,0 +1,430 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'workout_execution_screen.dart';
|
||||
|
||||
final class HistoryListScreen extends StatefulWidget {
|
||||
const HistoryListScreen({
|
||||
required this.historyUseCases,
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.activeUseCases,
|
||||
required this.closeUseCase,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WorkoutHistoryUseCases historyUseCases;
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
|
||||
@override
|
||||
State<HistoryListScreen> createState() => _HistoryListScreenState();
|
||||
}
|
||||
|
||||
final class _HistoryListScreenState extends State<HistoryListScreen> {
|
||||
late Future<List<WorkoutHistory>> _histories;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_histories = widget.historyUseCases.listActive();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Historique')),
|
||||
body: FutureBuilder<List<WorkoutHistory>>(
|
||||
future: _histories,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final histories = snapshot.data ?? const <WorkoutHistory>[];
|
||||
if (histories.isEmpty) {
|
||||
return const _CenteredMessage(
|
||||
title: 'Aucune séance terminée',
|
||||
message: 'Les séances terminées apparaîtront ici.',
|
||||
);
|
||||
}
|
||||
final groups = groupHistoriesByPeriod(histories, DateTime.now());
|
||||
return ListView(
|
||||
children: [
|
||||
for (final group in groups.entries) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 8),
|
||||
child: Text(
|
||||
group.key,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
for (final history in group.value)
|
||||
ListTile(
|
||||
title: Text(history.nameSnapshot),
|
||||
subtitle: Text(
|
||||
'${_formatDateTime(history.startedAt)} · '
|
||||
'${_formatDurationMs(history.totalActiveMs)} · '
|
||||
'${_historySummary(history)}',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _openDetail(history),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openDetail(WorkoutHistory history) async {
|
||||
final deleted = await Navigator.of(context).push<bool>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => HistoryDetailScreen(
|
||||
history: history,
|
||||
historyUseCases: widget.historyUseCases,
|
||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||
activeUseCases: widget.activeUseCases,
|
||||
closeUseCase: widget.closeUseCase,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (deleted == true) {
|
||||
setState(() {
|
||||
_histories = widget.historyUseCases.listActive();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class HistoryDetailScreen extends StatelessWidget {
|
||||
const HistoryDetailScreen({
|
||||
required this.history,
|
||||
required this.historyUseCases,
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.activeUseCases,
|
||||
required this.closeUseCase,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WorkoutHistory history;
|
||||
final WorkoutHistoryUseCases historyUseCases;
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final detail = HistoryDetailData.fromHistory(history);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(history.nameSnapshot)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Text(_formatDateTime(history.startedAt)),
|
||||
const SizedBox(height: 4),
|
||||
Text('Durée : ${_formatDurationMs(history.totalActiveMs)}'),
|
||||
const SizedBox(height: 16),
|
||||
for (final program in detail.programs) ...[
|
||||
Text(program.name, style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
for (final exercise in program.exercises) ...[
|
||||
Text(
|
||||
exercise.name,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
for (final set in exercise.sets)
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: Text('Série ${set.setIndex + 1}'),
|
||||
subtitle: Text(set.summary),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () => _restart(context),
|
||||
child: const Text('Relancer cette séance'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => _confirmDelete(context),
|
||||
child: const Text("Supprimer de l'historique"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _restart(BuildContext context) async {
|
||||
ActiveWorkoutSession session;
|
||||
final sourceId = history.sourceWorkoutTemplateId;
|
||||
if (sourceId != null &&
|
||||
await workoutTemplateUseCases.findById(sourceId) != null) {
|
||||
session = await activeUseCases.startFromTemplate(sourceId);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
"La séance originale n'existe plus. Une copie va être utilisée.",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
session = await activeUseCases.startFromHistory(history);
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WorkoutExecutionScreen(
|
||||
initialSession: session,
|
||||
activeUseCases: activeUseCases,
|
||||
closeUseCase: closeUseCase,
|
||||
historyUseCases: historyUseCases,
|
||||
workoutTemplateUseCases: workoutTemplateUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(BuildContext context) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("Supprimer de l'historique ?"),
|
||||
content: const Text('Cette action est définitive.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
await historyUseCases.delete(history.metadata.id);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
}
|
||||
|
||||
final class HistoryDetailData {
|
||||
const HistoryDetailData({required this.programs});
|
||||
|
||||
factory HistoryDetailData.fromHistory(WorkoutHistory history) {
|
||||
final snapshot =
|
||||
jsonDecode(history.historySnapshotJson) as Map<String, dynamic>;
|
||||
final resolved =
|
||||
jsonDecode(snapshot['resolvedTemplateSnapshotJson'] as String)
|
||||
as Map<String, dynamic>;
|
||||
final rawResults = history.results.isNotEmpty
|
||||
? history.results.map(_resultFromDomain).toList()
|
||||
: (snapshot['results'] as List<dynamic>? ?? const [])
|
||||
.cast<Map<String, dynamic>>();
|
||||
final programs = <HistoryProgramDetail>[];
|
||||
final rawPrograms = resolved['programs'] as List<dynamic>? ?? const [];
|
||||
for (
|
||||
var programIndex = 0;
|
||||
programIndex < rawPrograms.length;
|
||||
programIndex++
|
||||
) {
|
||||
final rawProgram = rawPrograms[programIndex] as Map<String, dynamic>;
|
||||
final programSnapshot =
|
||||
jsonDecode(rawProgram['programSnapshotJson'] as String)
|
||||
as Map<String, dynamic>;
|
||||
final exercises = <HistoryExerciseDetail>[];
|
||||
final rawExercises =
|
||||
programSnapshot['exercises'] as List<dynamic>? ?? const [];
|
||||
for (
|
||||
var exerciseIndex = 0;
|
||||
exerciseIndex < rawExercises.length;
|
||||
exerciseIndex++
|
||||
) {
|
||||
final rawExercise = rawExercises[exerciseIndex] as Map<String, dynamic>;
|
||||
final exerciseResults = rawResults.where((result) {
|
||||
return result['programIndex'] == programIndex &&
|
||||
result['exerciseIndex'] == exerciseIndex;
|
||||
}).toList();
|
||||
exercises.add(
|
||||
HistoryExerciseDetail(
|
||||
name: rawExercise['exerciseNameSnapshot'] as String,
|
||||
sets: [
|
||||
for (final result in exerciseResults)
|
||||
HistorySetDetail.fromSnapshot(rawExercise, result),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
programs.add(
|
||||
HistoryProgramDetail(
|
||||
name: rawProgram['programNameSnapshot'] as String,
|
||||
exercises: exercises,
|
||||
),
|
||||
);
|
||||
}
|
||||
return HistoryDetailData(programs: programs);
|
||||
}
|
||||
|
||||
final List<HistoryProgramDetail> programs;
|
||||
}
|
||||
|
||||
final class HistoryProgramDetail {
|
||||
const HistoryProgramDetail({required this.name, required this.exercises});
|
||||
|
||||
final String name;
|
||||
final List<HistoryExerciseDetail> exercises;
|
||||
}
|
||||
|
||||
final class HistoryExerciseDetail {
|
||||
const HistoryExerciseDetail({required this.name, required this.sets});
|
||||
|
||||
final String name;
|
||||
final List<HistorySetDetail> sets;
|
||||
}
|
||||
|
||||
final class HistorySetDetail {
|
||||
const HistorySetDetail({required this.setIndex, required this.summary});
|
||||
|
||||
factory HistorySetDetail.fromSnapshot(
|
||||
Map<String, dynamic> exercise,
|
||||
Map<String, dynamic> result,
|
||||
) {
|
||||
final parts = <String>[];
|
||||
if (exercise['timeEnabled'] == true && result['actualTimeMs'] != null) {
|
||||
parts.add('Temps ${_formatDurationMs(result['actualTimeMs'] as int)}');
|
||||
}
|
||||
if (exercise['repsEnabled'] == true && result['actualReps'] != null) {
|
||||
parts.add('${result['actualReps']} répétitions');
|
||||
}
|
||||
if (exercise['scoreEnabled'] == true && result['actualScore'] != null) {
|
||||
final unit = exercise['scoreUnitSnapshot'] as String?;
|
||||
final value = _formatScoreValue(result['actualScore'] as num);
|
||||
parts.add(unit == null ? 'Score $value' : 'Score $value $unit');
|
||||
}
|
||||
return HistorySetDetail(
|
||||
setIndex: result['setIndex'] as int,
|
||||
summary: parts.isEmpty ? 'Aucune mesure saisie' : parts.join(' · '),
|
||||
);
|
||||
}
|
||||
|
||||
final int setIndex;
|
||||
final String summary;
|
||||
}
|
||||
|
||||
Map<String, List<WorkoutHistory>> groupHistoriesByPeriod(
|
||||
List<WorkoutHistory> histories,
|
||||
DateTime now,
|
||||
) {
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final weekStart = today.subtract(Duration(days: today.weekday - 1));
|
||||
final groups = <String, List<WorkoutHistory>>{
|
||||
"Aujourd'hui": [],
|
||||
'Cette semaine': [],
|
||||
'Plus ancien': [],
|
||||
};
|
||||
for (final history in histories) {
|
||||
final startedDay = DateTime(
|
||||
history.startedAt.year,
|
||||
history.startedAt.month,
|
||||
history.startedAt.day,
|
||||
);
|
||||
if (startedDay == today) {
|
||||
groups["Aujourd'hui"]!.add(history);
|
||||
} else if (startedDay.isAfter(weekStart) || startedDay == weekStart) {
|
||||
groups['Cette semaine']!.add(history);
|
||||
} else {
|
||||
groups['Plus ancien']!.add(history);
|
||||
}
|
||||
}
|
||||
groups.removeWhere((key, value) => value.isEmpty);
|
||||
return groups;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _resultFromDomain(WorkoutHistorySetResult result) {
|
||||
return {
|
||||
'programIndex': result.programIndex,
|
||||
'exerciseIndex': result.exerciseIndex,
|
||||
'setIndex': result.setIndex,
|
||||
'actualTimeMs': result.actualTimeMs,
|
||||
'actualReps': result.actualReps,
|
||||
'actualScore': result.actualScore,
|
||||
};
|
||||
}
|
||||
|
||||
String _historySummary(WorkoutHistory history) {
|
||||
final snapshot =
|
||||
jsonDecode(history.historySnapshotJson) as Map<String, dynamic>;
|
||||
final rawResults = history.results.isNotEmpty
|
||||
? history.results.map(_resultFromDomain).toList()
|
||||
: (snapshot['results'] as List<dynamic>? ?? const [])
|
||||
.cast<Map<String, dynamic>>();
|
||||
final scoreCount = rawResults
|
||||
.where((result) => result['actualScore'] != null)
|
||||
.length;
|
||||
return '${rawResults.length} séries · $scoreCount score${scoreCount > 1 ? 's' : ''}';
|
||||
}
|
||||
|
||||
String _formatScoreValue(num value) {
|
||||
if (value is int || value == value.roundToDouble()) {
|
||||
return value.round().toString();
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime value) {
|
||||
final day = value.day.toString().padLeft(2, '0');
|
||||
final month = value.month.toString().padLeft(2, '0');
|
||||
final hour = value.hour.toString().padLeft(2, '0');
|
||||
final minute = value.minute.toString().padLeft(2, '0');
|
||||
return '$day/$month/${value.year} $hour:$minute';
|
||||
}
|
||||
|
||||
String _formatDurationMs(int milliseconds) {
|
||||
final duration = Duration(milliseconds: milliseconds);
|
||||
final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
final hours = duration.inHours;
|
||||
if (hours > 0) {
|
||||
return '$hours:$minutes:$seconds';
|
||||
}
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
|
||||
final class _CenteredMessage extends StatelessWidget {
|
||||
const _CenteredMessage({required this.title, this.message});
|
||||
|
||||
final String title;
|
||||
final String? message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(message!, textAlign: TextAlign.center),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import '../application/app_bootstrap.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'exercise_library_screen.dart';
|
||||
import 'history_screen.dart';
|
||||
import 'program_screen.dart';
|
||||
import 'workout_execution_screen.dart';
|
||||
import 'workout_template_screen.dart';
|
||||
@ -97,6 +98,23 @@ final class _HomeScreenState extends State<HomeScreen> {
|
||||
programUseCases: widget.bootstrap.programUseCases,
|
||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: const Text('Historique'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => HistoryListScreen(
|
||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||
workoutTemplateUseCases:
|
||||
widget.bootstrap.workoutTemplateUseCases,
|
||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -113,6 +131,8 @@ final class _HomeScreenState extends State<HomeScreen> {
|
||||
initialSession: session,
|
||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export 'game_time_app.dart';
|
||||
export 'exercise_library_screen.dart';
|
||||
export 'history_screen.dart';
|
||||
export 'home_screen.dart';
|
||||
export 'program_screen.dart';
|
||||
export 'workout_execution_screen.dart';
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'history_screen.dart';
|
||||
|
||||
enum WorkoutExecutionMode { active, rest, paused, finished }
|
||||
|
||||
@ -13,12 +14,16 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
||||
required this.initialSession,
|
||||
required this.activeUseCases,
|
||||
required this.closeUseCase,
|
||||
required this.historyUseCases,
|
||||
required this.workoutTemplateUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ActiveWorkoutSession initialSession;
|
||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final WorkoutHistoryUseCases historyUseCases;
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
|
||||
@override
|
||||
State<WorkoutExecutionScreen> createState() => _WorkoutExecutionScreenState();
|
||||
@ -30,6 +35,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
late WorkoutExecutionMode _mode;
|
||||
Timer? _ticker;
|
||||
Timer? _restTicker;
|
||||
WorkoutHistory? _completedHistory;
|
||||
var _reps = 0;
|
||||
final _scoreController = TextEditingController();
|
||||
var _remainingRestSeconds = 0;
|
||||
@ -232,9 +238,12 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
const SizedBox(height: 12),
|
||||
Text('Temps total : ${_formatDuration(elapsed)}'),
|
||||
const SizedBox(height: 24),
|
||||
OutlinedButton(onPressed: () {}, child: const Text('Voir le détail')),
|
||||
OutlinedButton(
|
||||
onPressed: () {},
|
||||
onPressed: _completedHistory == null ? null : _openHistoryDetail,
|
||||
child: const Text('Voir le détail'),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: _completedHistory == null ? null : _restartCompleted,
|
||||
child: const Text('Relancer cette séance'),
|
||||
),
|
||||
FilledButton(
|
||||
@ -367,7 +376,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
|
||||
Future<void> _complete() async {
|
||||
_session = await widget.activeUseCases.complete(_session.metadata.id);
|
||||
await widget.closeUseCase.close(
|
||||
_completedHistory = await widget.closeUseCase.close(
|
||||
sessionId: _session.metadata.id,
|
||||
nameSnapshot: _plan.name,
|
||||
completed: true,
|
||||
@ -375,6 +384,56 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
if (!mounted) return;
|
||||
setState(() => _mode = WorkoutExecutionMode.finished);
|
||||
}
|
||||
|
||||
Future<void> _openHistoryDetail() async {
|
||||
final history = _completedHistory;
|
||||
if (history == null) return;
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => HistoryDetailScreen(
|
||||
history: history,
|
||||
historyUseCases: widget.historyUseCases,
|
||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||
activeUseCases: widget.activeUseCases,
|
||||
closeUseCase: widget.closeUseCase,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _restartCompleted() async {
|
||||
final history = _completedHistory;
|
||||
if (history == null) return;
|
||||
ActiveWorkoutSession session;
|
||||
final sourceId = history.sourceWorkoutTemplateId;
|
||||
if (sourceId != null &&
|
||||
await widget.workoutTemplateUseCases.findById(sourceId) != null) {
|
||||
session = await widget.activeUseCases.startFromTemplate(sourceId);
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
"La séance originale n'existe plus. Une copie va être utilisée.",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
session = await widget.activeUseCases.startFromHistory(history);
|
||||
}
|
||||
if (!mounted) return;
|
||||
await Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WorkoutExecutionScreen(
|
||||
initialSession: session,
|
||||
activeUseCases: widget.activeUseCases,
|
||||
closeUseCase: widget.closeUseCase,
|
||||
historyUseCases: widget.historyUseCases,
|
||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class SetMeasureInput extends StatelessWidget {
|
||||
|
||||
@ -12,6 +12,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
required this.programUseCases,
|
||||
required this.activeUseCases,
|
||||
required this.closeUseCase,
|
||||
required this.historyUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -19,6 +20,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
final ProgramUseCases programUseCases;
|
||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final WorkoutHistoryUseCases historyUseCases;
|
||||
|
||||
@override
|
||||
State<WorkoutTemplateListScreen> createState() =>
|
||||
@ -119,6 +121,8 @@ final class _WorkoutTemplateListScreenState
|
||||
initialSession: session,
|
||||
activeUseCases: widget.activeUseCases,
|
||||
closeUseCase: widget.closeUseCase,
|
||||
historyUseCases: widget.historyUseCases,
|
||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
305
test/presentation/history_screen_test.dart
Normal file
305
test/presentation/history_screen_test.dart
Normal file
@ -0,0 +1,305 @@
|
||||
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('la liste groupe les séances par période', (tester) async {
|
||||
final groups = groupHistoriesByPeriod([
|
||||
_history(id: 'today', startedAt: DateTime.utc(2026, 7, 17, 9)),
|
||||
_history(id: 'week', startedAt: DateTime.utc(2026, 7, 15, 9)),
|
||||
_history(id: 'old', startedAt: DateTime.utc(2026, 7, 1, 9)),
|
||||
], DateTime.utc(2026, 7, 17, 12));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Column(children: [for (final key in groups.keys) Text(key)]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text("Aujourd'hui"), findsOneWidget);
|
||||
expect(find.text('Cette semaine'), findsOneWidget);
|
||||
expect(find.text('Plus ancien'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('le détail affiche temps répétitions et score avec unité', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: HistoryDetailScreen(
|
||||
history: _history(id: 'history-1'),
|
||||
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||
activeUseCases: _activeUseCases(),
|
||||
closeUseCase: _closeUseCase(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Programme jambes'), findsOneWidget);
|
||||
expect(find.text('Squat'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Temps 00:45 · 10 répétitions · Score 80 kg'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('la suppression demande une confirmation', (tester) async {
|
||||
final repository = _FakeWorkoutHistoryRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: HistoryDetailScreen(
|
||||
history: _history(id: 'history-1'),
|
||||
historyUseCases: _historyUseCases(repository),
|
||||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||
activeUseCases: _activeUseCases(),
|
||||
closeUseCase: _closeUseCase(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text("Supprimer de l'historique"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.deletedIds, isEmpty);
|
||||
expect(find.text("Supprimer de l'historique ?"), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Annuler'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.deletedIds, isEmpty);
|
||||
|
||||
await tester.tap(find.text("Supprimer de l'historique"));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(repository.deletedIds, ['history-1']);
|
||||
});
|
||||
}
|
||||
|
||||
WorkoutHistory _history({required String id, DateTime? startedAt}) {
|
||||
final start = startedAt ?? DateTime.utc(2026, 7, 17, 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': _resolvedSnapshot(),
|
||||
'results': [
|
||||
{
|
||||
'programSnapshotId': 'program-snapshot-1',
|
||||
'exerciseSnapshotId': 'exercise-snapshot-1',
|
||||
'programIndex': 0,
|
||||
'exerciseIndex': 0,
|
||||
'setIndex': 0,
|
||||
'actualTimeMs': 45000,
|
||||
'actualReps': 10,
|
||||
'actualScore': 80,
|
||||
'scoreLabelSnapshot': 'Charge',
|
||||
'scoreUnitSnapshot': 'kg',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
String _resolvedSnapshot() {
|
||||
return 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,
|
||||
'scoreLabelSnapshot': 'Charge',
|
||||
'scoreUnitSnapshot': 'kg',
|
||||
'restSecondsOverride': 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
'overrides': const [],
|
||||
});
|
||||
}
|
||||
|
||||
EntityMetadata _metadata(String id) {
|
||||
return EntityMetadata(
|
||||
id: id,
|
||||
createdAt: DateTime.utc(2026, 7, 17),
|
||||
updatedAt: DateTime.utc(2026, 7, 17),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
WorkoutHistoryUseCases _historyUseCases(
|
||||
_FakeWorkoutHistoryRepository repository,
|
||||
) {
|
||||
return WorkoutHistoryUseCases(
|
||||
repository: repository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||||
);
|
||||
}
|
||||
|
||||
WorkoutTemplateUseCases _workoutTemplateUseCases() {
|
||||
return WorkoutTemplateUseCases(
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
programRepository: _FakeProgramRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
ActiveWorkoutSessionUseCases _activeUseCases() {
|
||||
return ActiveWorkoutSessionUseCases(
|
||||
sessionRepository: _FakeActiveSessionRepository(),
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
CloseWorkoutSessionUseCase _closeUseCase() {
|
||||
return CloseWorkoutSessionUseCase(
|
||||
sessionRepository: _FakeActiveSessionRepository(),
|
||||
historyRepository: _FakeWorkoutHistoryRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
final class _FakeClock implements Clock {
|
||||
_FakeClock(this.value);
|
||||
|
||||
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 deletedIds = <String>[];
|
||||
|
||||
@override
|
||||
Future<void> delete(String id, DateTime deletedAt) async {
|
||||
deletedIds.add(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WorkoutHistory?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<WorkoutHistory>> listActive() async => const [];
|
||||
|
||||
@override
|
||||
Future<void> save(WorkoutHistory history) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult 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<ActiveWorkoutSession?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findOpen() async => null;
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveRestState(ActiveRestState restState) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||||
}
|
||||
@ -33,6 +33,8 @@ void main() {
|
||||
clock,
|
||||
),
|
||||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||||
historyUseCases: _historyUseCases(clock),
|
||||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -141,6 +143,23 @@ CloseWorkoutSessionUseCase _closeUseCase(
|
||||
);
|
||||
}
|
||||
|
||||
WorkoutHistoryUseCases _historyUseCases(_FakeClock clock) {
|
||||
return WorkoutHistoryUseCases(
|
||||
repository: _FakeWorkoutHistoryRepository(),
|
||||
clock: clock,
|
||||
);
|
||||
}
|
||||
|
||||
WorkoutTemplateUseCases _workoutTemplateUseCases() {
|
||||
return WorkoutTemplateUseCases(
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
programRepository: _FakeProgramRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
String _sessionSnapshot({int setsCount = 3, int restSeconds = 0}) {
|
||||
return jsonEncode({
|
||||
'name': 'Séance jambes',
|
||||
@ -260,9 +279,35 @@ final class _FakeWorkoutTemplateRepository
|
||||
}
|
||||
|
||||
final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
@override
|
||||
Future<void> delete(String id, DateTime deletedAt) async {}
|
||||
|
||||
@override
|
||||
Future<WorkoutHistory?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<WorkoutHistory>> listActive() async => const [];
|
||||
|
||||
@override
|
||||
Future<void> save(WorkoutHistory history) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||||
}
|
||||
|
||||
final class _FakeProgramRepository implements ProgramRepository {
|
||||
@override
|
||||
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
|
||||
|
||||
@override
|
||||
Future<Program?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<Program>> listActive() async => const [];
|
||||
|
||||
@override
|
||||
Future<void> save(Program program) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user