Corrige l'affichage des images (#41) et de la vidéo (#42) dans le carrousel de médias, et le format du chrono de score qui n'affichait que les secondes (#43) sur workout_execution_screen.dart. Ajoute la dépendance video_player (2.13.0) pour la lecture vidéo. Ajuste home_screen.dart, history_screen.dart et workout_template_screen.dart en conséquence. flutter analyze propre, flutter test 77/77 vert (plus aucun hang), pub get résolu. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -350,6 +350,10 @@ final class MediaUseCases {
|
|||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<MediaAsset?> findById(String id) {
|
||||||
|
return mediaRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Exercise> attachToExercise({
|
Future<Exercise> attachToExercise({
|
||||||
required String exerciseId,
|
required String exerciseId,
|
||||||
required String mediaAssetId,
|
required String mediaAssetId,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
required this.workoutTemplateUseCases,
|
required this.workoutTemplateUseCases,
|
||||||
required this.activeUseCases,
|
required this.activeUseCases,
|
||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
|
this.mediaUseCases,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
|
final MediaUseCases? mediaUseCases;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HistoryListScreen> createState() => _HistoryListScreenState();
|
State<HistoryListScreen> createState() => _HistoryListScreenState();
|
||||||
@ -93,6 +95,7 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
|
|||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
|
mediaUseCases: widget.mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -111,6 +114,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
required this.workoutTemplateUseCases,
|
required this.workoutTemplateUseCases,
|
||||||
required this.activeUseCases,
|
required this.activeUseCases,
|
||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
|
this.mediaUseCases,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -119,6 +123,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
|
final MediaUseCases? mediaUseCases;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -191,6 +196,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
closeUseCase: closeUseCase,
|
closeUseCase: closeUseCase,
|
||||||
historyUseCases: historyUseCases,
|
historyUseCases: historyUseCases,
|
||||||
workoutTemplateUseCases: workoutTemplateUseCases,
|
workoutTemplateUseCases: workoutTemplateUseCases,
|
||||||
|
mediaUseCases: mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -154,6 +154,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||||
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -170,6 +171,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
widget.bootstrap.workoutTemplateUseCases,
|
widget.bootstrap.workoutTemplateUseCases,
|
||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -188,6 +190,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||||
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
||||||
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
import '../application/application.dart';
|
import '../application/application.dart';
|
||||||
import '../domain/domain.dart';
|
import '../domain/domain.dart';
|
||||||
@ -10,6 +12,8 @@ import 'theme.dart';
|
|||||||
|
|
||||||
enum WorkoutExecutionMode { active, rest, paused, finished }
|
enum WorkoutExecutionMode { active, rest, paused, finished }
|
||||||
|
|
||||||
|
typedef VideoMediaBuilder = Widget Function(BuildContext context, MediaAsset asset);
|
||||||
|
|
||||||
final class WorkoutExecutionScreen extends StatefulWidget {
|
final class WorkoutExecutionScreen extends StatefulWidget {
|
||||||
const WorkoutExecutionScreen({
|
const WorkoutExecutionScreen({
|
||||||
required this.initialSession,
|
required this.initialSession,
|
||||||
@ -17,6 +21,9 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
|||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
required this.historyUseCases,
|
required this.historyUseCases,
|
||||||
required this.workoutTemplateUseCases,
|
required this.workoutTemplateUseCases,
|
||||||
|
this.mediaUseCases,
|
||||||
|
this.mediaAssetLoader,
|
||||||
|
this.videoMediaBuilder,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -25,6 +32,9 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
|||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final WorkoutHistoryUseCases historyUseCases;
|
final WorkoutHistoryUseCases historyUseCases;
|
||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
|
final MediaUseCases? mediaUseCases;
|
||||||
|
final Future<MediaAsset?> Function(String id)? mediaAssetLoader;
|
||||||
|
final VideoMediaBuilder? videoMediaBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<WorkoutExecutionScreen> createState() => _WorkoutExecutionScreenState();
|
State<WorkoutExecutionScreen> createState() => _WorkoutExecutionScreenState();
|
||||||
@ -36,6 +46,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
late WorkoutExecutionMode _mode;
|
late WorkoutExecutionMode _mode;
|
||||||
Timer? _ticker;
|
Timer? _ticker;
|
||||||
Timer? _restTicker;
|
Timer? _restTicker;
|
||||||
|
Timer? _scoreStopwatchTicker;
|
||||||
WorkoutHistory? _completedHistory;
|
WorkoutHistory? _completedHistory;
|
||||||
String? _activeRestStateId;
|
String? _activeRestStateId;
|
||||||
ActiveScoreStopwatchState? _scoreStopwatch;
|
ActiveScoreStopwatchState? _scoreStopwatch;
|
||||||
@ -73,6 +84,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_ticker?.cancel();
|
_ticker?.cancel();
|
||||||
_restTicker?.cancel();
|
_restTicker?.cancel();
|
||||||
|
_scoreStopwatchTicker?.cancel();
|
||||||
_scoreController.dispose();
|
_scoreController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -339,10 +351,20 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
restRemainingSeconds: _mode == WorkoutExecutionMode.rest
|
restRemainingSeconds: _mode == WorkoutExecutionMode.rest
|
||||||
? _remainingRestSeconds
|
? _remainingRestSeconds
|
||||||
: null,
|
: null,
|
||||||
|
mediaAssetLoader: _loadMediaAsset,
|
||||||
|
videoMediaBuilder: widget.videoMediaBuilder,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<MediaAsset?> _loadMediaAsset(String id) {
|
||||||
|
final loader = widget.mediaAssetLoader;
|
||||||
|
if (loader != null) return loader(id);
|
||||||
|
final mediaUseCases = widget.mediaUseCases;
|
||||||
|
if (mediaUseCases == null) return Future<MediaAsset?>.value();
|
||||||
|
return mediaUseCases.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
int get _scoreStopwatchElapsedMs {
|
int get _scoreStopwatchElapsedMs {
|
||||||
final manual = _manualScoreTimeMs;
|
final manual = _manualScoreTimeMs;
|
||||||
if (manual != null) {
|
if (manual != null) {
|
||||||
@ -359,6 +381,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
if (!_exercise.stopwatchScoreEnabled) {
|
if (!_exercise.stopwatchScoreEnabled) {
|
||||||
_scoreStopwatch = null;
|
_scoreStopwatch = null;
|
||||||
_manualScoreTimeMs = null;
|
_manualScoreTimeMs = null;
|
||||||
|
_refreshScoreStopwatchTicker();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final state = await widget.activeUseCases.findScoreStopwatch(
|
final state = await widget.activeUseCases.findScoreStopwatch(
|
||||||
@ -369,6 +392,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _scoreStopwatch = state);
|
setState(() => _scoreStopwatch = state);
|
||||||
|
_refreshScoreStopwatchTicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startScoreStopwatch() async {
|
Future<void> _startScoreStopwatch() async {
|
||||||
@ -383,6 +407,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
_scoreStopwatch = state;
|
_scoreStopwatch = state;
|
||||||
_manualScoreTimeMs = null;
|
_manualScoreTimeMs = null;
|
||||||
});
|
});
|
||||||
|
_refreshScoreStopwatchTicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ActiveScoreStopwatchState?> _stopScoreStopwatch() async {
|
Future<ActiveScoreStopwatchState?> _stopScoreStopwatch() async {
|
||||||
@ -396,6 +421,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
);
|
);
|
||||||
if (!mounted) return stopped;
|
if (!mounted) return stopped;
|
||||||
setState(() => _scoreStopwatch = stopped);
|
setState(() => _scoreStopwatch = stopped);
|
||||||
|
_refreshScoreStopwatchTicker();
|
||||||
return stopped;
|
return stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,6 +437,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
_scoreStopwatch = state;
|
_scoreStopwatch = state;
|
||||||
_manualScoreTimeMs = null;
|
_manualScoreTimeMs = null;
|
||||||
});
|
});
|
||||||
|
_refreshScoreStopwatchTicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _resetScoreStopwatch() async {
|
Future<void> _resetScoreStopwatch() async {
|
||||||
@ -445,6 +472,23 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
_scoreStopwatch = null;
|
_scoreStopwatch = null;
|
||||||
_manualScoreTimeMs = null;
|
_manualScoreTimeMs = null;
|
||||||
});
|
});
|
||||||
|
_refreshScoreStopwatchTicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _refreshScoreStopwatchTicker() {
|
||||||
|
final shouldTick =
|
||||||
|
_scoreStopwatch?.status == ActiveScoreStopwatchStatus.running;
|
||||||
|
if (!shouldTick) {
|
||||||
|
_scoreStopwatchTicker?.cancel();
|
||||||
|
_scoreStopwatchTicker = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_scoreStopwatchTicker != null) return;
|
||||||
|
_scoreStopwatchTicker = Timer.periodic(const Duration(milliseconds: 100), (
|
||||||
|
_,
|
||||||
|
) {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _editScoreStopwatchTime() async {
|
Future<void> _editScoreStopwatchTime() async {
|
||||||
@ -575,6 +619,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
_scoreStopwatch = null;
|
_scoreStopwatch = null;
|
||||||
_manualScoreTimeMs = null;
|
_manualScoreTimeMs = null;
|
||||||
});
|
});
|
||||||
|
_refreshScoreStopwatchTicker();
|
||||||
}
|
}
|
||||||
if (!skipped &&
|
if (!skipped &&
|
||||||
_exercise.stopwatchScoreEnabled &&
|
_exercise.stopwatchScoreEnabled &&
|
||||||
@ -767,6 +812,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
_activeRestStateId = null;
|
_activeRestStateId = null;
|
||||||
_scoreStopwatch = null;
|
_scoreStopwatch = null;
|
||||||
_manualScoreTimeMs = null;
|
_manualScoreTimeMs = null;
|
||||||
|
_refreshScoreStopwatchTicker();
|
||||||
_seriesStartedAt = DateTime.now().toUtc();
|
_seriesStartedAt = DateTime.now().toUtc();
|
||||||
_session = await widget.activeUseCases.updateProgress(
|
_session = await widget.activeUseCases.updateProgress(
|
||||||
sessionId: _session.metadata.id,
|
sessionId: _session.metadata.id,
|
||||||
@ -836,6 +882,9 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
historyUseCases: widget.historyUseCases,
|
historyUseCases: widget.historyUseCases,
|
||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
|
mediaUseCases: widget.mediaUseCases,
|
||||||
|
mediaAssetLoader: widget.mediaAssetLoader,
|
||||||
|
videoMediaBuilder: widget.videoMediaBuilder,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -1256,10 +1305,14 @@ enum _MediaSheetSection { images, video }
|
|||||||
final class _ExerciseMediaSheet extends StatefulWidget {
|
final class _ExerciseMediaSheet extends StatefulWidget {
|
||||||
const _ExerciseMediaSheet({
|
const _ExerciseMediaSheet({
|
||||||
required this.exercise,
|
required this.exercise,
|
||||||
|
required this.mediaAssetLoader,
|
||||||
|
this.videoMediaBuilder,
|
||||||
this.restRemainingSeconds,
|
this.restRemainingSeconds,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ExecutionExercise exercise;
|
final ExecutionExercise exercise;
|
||||||
|
final Future<MediaAsset?> Function(String id) mediaAssetLoader;
|
||||||
|
final VideoMediaBuilder? videoMediaBuilder;
|
||||||
final int? restRemainingSeconds;
|
final int? restRemainingSeconds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1346,9 +1399,14 @@ final class _ExerciseMediaSheetState extends State<_ExerciseMediaSheet> {
|
|||||||
pageController: _pageController,
|
pageController: _pageController,
|
||||||
currentIndex: _imageIndex,
|
currentIndex: _imageIndex,
|
||||||
onPageChanged: (index) => setState(() => _imageIndex = index),
|
onPageChanged: (index) => setState(() => _imageIndex = index),
|
||||||
|
mediaAssetLoader: widget.mediaAssetLoader,
|
||||||
)
|
)
|
||||||
else if (hasVideo)
|
else if (hasVideo)
|
||||||
_VideoMediaPanel(videoMediaId: exercise.videoMediaId!),
|
_VideoMediaPanel(
|
||||||
|
videoMediaId: exercise.videoMediaId!,
|
||||||
|
mediaAssetLoader: widget.mediaAssetLoader,
|
||||||
|
videoMediaBuilder: widget.videoMediaBuilder,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1362,12 +1420,14 @@ final class _ImageMediaGallery extends StatelessWidget {
|
|||||||
required this.pageController,
|
required this.pageController,
|
||||||
required this.currentIndex,
|
required this.currentIndex,
|
||||||
required this.onPageChanged,
|
required this.onPageChanged,
|
||||||
|
required this.mediaAssetLoader,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<String> imageMediaIds;
|
final List<String> imageMediaIds;
|
||||||
final PageController pageController;
|
final PageController pageController;
|
||||||
final int currentIndex;
|
final int currentIndex;
|
||||||
final ValueChanged<int> onPageChanged;
|
final ValueChanged<int> onPageChanged;
|
||||||
|
final Future<MediaAsset?> Function(String id) mediaAssetLoader;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -1396,6 +1456,7 @@ final class _ImageMediaGallery extends StatelessWidget {
|
|||||||
return _ImageMediaPage(
|
return _ImageMediaPage(
|
||||||
mediaAssetId: imageMediaIds[index],
|
mediaAssetId: imageMediaIds[index],
|
||||||
index: index,
|
index: index,
|
||||||
|
mediaAssetLoader: mediaAssetLoader,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -1407,10 +1468,15 @@ final class _ImageMediaGallery extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class _ImageMediaPage extends StatelessWidget {
|
final class _ImageMediaPage extends StatelessWidget {
|
||||||
const _ImageMediaPage({required this.mediaAssetId, required this.index});
|
const _ImageMediaPage({
|
||||||
|
required this.mediaAssetId,
|
||||||
|
required this.index,
|
||||||
|
required this.mediaAssetLoader,
|
||||||
|
});
|
||||||
|
|
||||||
final String mediaAssetId;
|
final String mediaAssetId;
|
||||||
final int index;
|
final int index;
|
||||||
|
final Future<MediaAsset?> Function(String id) mediaAssetLoader;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -1422,55 +1488,201 @@ final class _ImageMediaPage extends StatelessWidget {
|
|||||||
border: Border.all(color: tokens.border),
|
border: Border.all(color: tokens.border),
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: FutureBuilder<MediaAsset?>(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
future: mediaAssetLoader(mediaAssetId),
|
||||||
children: [
|
builder: (context, snapshot) {
|
||||||
Icon(
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
Icons.image_outlined,
|
return const Center(child: CircularProgressIndicator());
|
||||||
size: 64,
|
}
|
||||||
color: Theme.of(context).colorScheme.primary,
|
final asset = snapshot.data;
|
||||||
|
if (asset == null || asset.kind != MediaKind.image) {
|
||||||
|
return _MissingMediaPlaceholder(
|
||||||
|
icon: Icons.broken_image_outlined,
|
||||||
|
title: 'Image indisponible',
|
||||||
|
subtitle: mediaAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
child: Image.file(
|
||||||
|
_fileFromLocalUri(asset.localUri),
|
||||||
|
key: ValueKey('exercise-image-${asset.metadata.id}'),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return _MissingMediaPlaceholder(
|
||||||
|
icon: Icons.broken_image_outlined,
|
||||||
|
title: 'Image introuvable',
|
||||||
|
subtitle: asset.metadata.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
);
|
||||||
Text(
|
},
|
||||||
'Image ${index + 1}',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(mediaAssetId, style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _VideoMediaPanel extends StatelessWidget {
|
final class _VideoMediaPanel extends StatelessWidget {
|
||||||
const _VideoMediaPanel({required this.videoMediaId});
|
const _VideoMediaPanel({
|
||||||
|
required this.videoMediaId,
|
||||||
|
required this.mediaAssetLoader,
|
||||||
|
this.videoMediaBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
final String videoMediaId;
|
final String videoMediaId;
|
||||||
|
final Future<MediaAsset?> Function(String id) mediaAssetLoader;
|
||||||
|
final VideoMediaBuilder? videoMediaBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CourtBlazerAccentPanel(
|
return CourtBlazerAccentPanel(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: SizedBox(
|
child: FutureBuilder<MediaAsset?>(
|
||||||
|
future: mediaAssetLoader(videoMediaId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const SizedBox(
|
||||||
height: 260,
|
height: 260,
|
||||||
child: Column(
|
child: Center(child: CircularProgressIndicator()),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
);
|
||||||
children: [
|
}
|
||||||
Icon(
|
final asset = snapshot.data;
|
||||||
Icons.play_circle_outline,
|
if (asset == null || asset.kind != MediaKind.video) {
|
||||||
size: 64,
|
return SizedBox(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
height: 260,
|
||||||
),
|
child: _MissingMediaPlaceholder(
|
||||||
const SizedBox(height: 16),
|
icon: Icons.videocam_off_outlined,
|
||||||
Text('Vidéo', style: Theme.of(context).textTheme.titleMedium),
|
title: 'Vidéo indisponible',
|
||||||
const SizedBox(height: 4),
|
subtitle: videoMediaId,
|
||||||
Text(videoMediaId, style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
final builder = videoMediaBuilder;
|
||||||
|
if (builder != null) {
|
||||||
|
return builder(context, asset);
|
||||||
|
}
|
||||||
|
return _VideoPlayerSurface(asset: asset);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _VideoPlayerSurface extends StatefulWidget {
|
||||||
|
const _VideoPlayerSurface({required this.asset});
|
||||||
|
|
||||||
|
final MediaAsset asset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_VideoPlayerSurface> createState() => _VideoPlayerSurfaceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _VideoPlayerSurfaceState extends State<_VideoPlayerSurface> {
|
||||||
|
late final VideoPlayerController _controller;
|
||||||
|
late final Future<void> _initialize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = VideoPlayerController.file(
|
||||||
|
_fileFromLocalUri(widget.asset.localUri),
|
||||||
|
);
|
||||||
|
_initialize = _controller.initialize().then((_) {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<void>(
|
||||||
|
future: _initialize,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 260,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.hasError || !_controller.value.isInitialized) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 260,
|
||||||
|
child: _MissingMediaPlaceholder(
|
||||||
|
icon: Icons.videocam_off_outlined,
|
||||||
|
title: 'Vidéo introuvable',
|
||||||
|
subtitle: widget.asset.metadata.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: _controller.value.aspectRatio,
|
||||||
|
child: VideoPlayer(_controller),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
IconButton.filled(
|
||||||
|
tooltip: _controller.value.isPlaying ? 'Pause' : 'Lecture',
|
||||||
|
onPressed: () async {
|
||||||
|
if (_controller.value.isPlaying) {
|
||||||
|
await _controller.pause();
|
||||||
|
} else {
|
||||||
|
await _controller.play();
|
||||||
|
}
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _MissingMediaPlaceholder extends StatelessWidget {
|
||||||
|
const _MissingMediaPlaceholder({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 64, color: Theme.of(context).colorScheme.primary),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File _fileFromLocalUri(String localUri) {
|
||||||
|
final uri = Uri.tryParse(localUri);
|
||||||
|
if (uri != null && uri.hasScheme) {
|
||||||
|
return File.fromUri(uri);
|
||||||
|
}
|
||||||
|
return File(localUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _WorkoutPlanSetTile extends StatelessWidget {
|
final class _WorkoutPlanSetTile extends StatelessWidget {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
|||||||
required this.activeUseCases,
|
required this.activeUseCases,
|
||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
required this.historyUseCases,
|
required this.historyUseCases,
|
||||||
|
this.mediaUseCases,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
|||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final WorkoutHistoryUseCases historyUseCases;
|
final WorkoutHistoryUseCases historyUseCases;
|
||||||
|
final MediaUseCases? mediaUseCases;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<WorkoutTemplateListScreen> createState() =>
|
State<WorkoutTemplateListScreen> createState() =>
|
||||||
@ -141,6 +143,7 @@ final class _WorkoutTemplateListScreenState
|
|||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
historyUseCases: widget.historyUseCases,
|
historyUseCases: widget.historyUseCases,
|
||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
|
mediaUseCases: widget.mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
56
pubspec.lock
56
pubspec.lock
@ -177,6 +177,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -344,6 +352,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -829,6 +845,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
video_player:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_player
|
||||||
|
sha256: "20ef311160e2ced020a62f38e4cc399a1bf289e722fc5fe37c9d7b18e44c9e8d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.0"
|
||||||
|
video_player_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_android
|
||||||
|
sha256: "5121de08444d00363efdda630a69ad4d27a208a9f7586482d4909a44a1cdbc63"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.0"
|
||||||
|
video_player_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_avfoundation
|
||||||
|
sha256: c238f5f0a26845cd0bcc2956049b63065a4d3c40ddfc22c3414cd170bef7fff9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.0"
|
||||||
|
video_player_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_platform_interface
|
||||||
|
sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.9.0"
|
||||||
|
video_player_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_web
|
||||||
|
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -17,6 +17,7 @@ dependencies:
|
|||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
sqlite3_flutter_libs: ^0.6.0+eol
|
sqlite3_flutter_libs: ^0.6.0+eol
|
||||||
|
video_player: ^2.11.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -153,6 +154,12 @@ void main() {
|
|||||||
testWidgets('le bouton médias ouvre la sheet si médias présents', (
|
testWidgets('le bouton médias ouvre la sheet si médias présents', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
final imageFile = (await tester.runAsync(_createTestImageFile))!;
|
||||||
|
addTearDown(() async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
await imageFile.parent.delete(recursive: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
final session = ActiveWorkoutSession(
|
final session = ActiveWorkoutSession(
|
||||||
metadata: _metadata('session-1'),
|
metadata: _metadata('session-1'),
|
||||||
@ -170,6 +177,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tester.runAsync(() async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: WorkoutExecutionScreen(
|
home: WorkoutExecutionScreen(
|
||||||
@ -181,23 +189,62 @@ void main() {
|
|||||||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||||||
historyUseCases: _historyUseCases(clock),
|
historyUseCases: _historyUseCases(clock),
|
||||||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
mediaAssetLoader: (id) async {
|
||||||
|
if (id == 'image-1') {
|
||||||
|
return MediaAsset(
|
||||||
|
metadata: _metadata(id),
|
||||||
|
kind: MediaKind.image,
|
||||||
|
localUri: imageFile.uri.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (id == 'video-1') {
|
||||||
|
return MediaAsset(
|
||||||
|
metadata: _metadata(id),
|
||||||
|
kind: MediaKind.video,
|
||||||
|
localUri: imageFile.uri.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
videoMediaBuilder: (context, asset) => SizedBox(
|
||||||
|
height: 260,
|
||||||
|
child: Center(child: Text('Lecteur vidéo ${asset.metadata.id}')),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
expect(find.byTooltip('Voir les médias de l’exercice'), findsOneWidget);
|
expect(find.byTooltip('Voir les médias de l’exercice'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('Voir médias'));
|
await tester.tap(find.text('Voir médias'));
|
||||||
|
await tester.runAsync(() async {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
expect(find.text('Médias de l’exercice'), findsOneWidget);
|
expect(find.text('Médias de l’exercice'), findsOneWidget);
|
||||||
expect(find.text('Images'), findsOneWidget);
|
expect(find.text('Images'), findsOneWidget);
|
||||||
expect(find.text('Vidéo'), findsOneWidget);
|
expect(find.text('Vidéo'), findsOneWidget);
|
||||||
expect(find.text('1/1'), findsOneWidget);
|
expect(find.text('1/1'), findsOneWidget);
|
||||||
expect(find.text('Image 1'), findsOneWidget);
|
expect(
|
||||||
|
find.byKey(const ValueKey('exercise-image-image-1')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.text('Image 1'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Vidéo'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Lecteur vidéo video-1'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('la galerie médias navigue entre les images', (tester) async {
|
testWidgets('la galerie médias navigue entre les images', (tester) async {
|
||||||
|
final imageFile = (await tester.runAsync(_createTestImageFile))!;
|
||||||
|
addTearDown(() async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
await imageFile.parent.delete(recursive: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
final session = ActiveWorkoutSession(
|
final session = ActiveWorkoutSession(
|
||||||
metadata: _metadata('session-1'),
|
metadata: _metadata('session-1'),
|
||||||
@ -214,6 +261,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tester.runAsync(() async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: WorkoutExecutionScreen(
|
home: WorkoutExecutionScreen(
|
||||||
@ -225,20 +273,33 @@ void main() {
|
|||||||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||||||
historyUseCases: _historyUseCases(clock),
|
historyUseCases: _historyUseCases(clock),
|
||||||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
mediaAssetLoader: (id) async => MediaAsset(
|
||||||
|
metadata: _metadata(id),
|
||||||
|
kind: MediaKind.image,
|
||||||
|
localUri: imageFile.uri.toString(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
await tester.tap(find.text('Voir médias'));
|
await tester.tap(find.text('Voir médias'));
|
||||||
|
await tester.runAsync(() async {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
expect(find.text('1/3'), findsOneWidget);
|
expect(find.text('1/3'), findsOneWidget);
|
||||||
|
|
||||||
await tester.drag(find.byType(PageView), const Offset(-400, 0));
|
await tester.drag(find.byType(PageView), const Offset(-400, 0));
|
||||||
|
await tester.runAsync(() async {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
expect(find.text('2/3'), findsOneWidget);
|
expect(find.text('2/3'), findsOneWidget);
|
||||||
expect(find.text('Image 2'), findsOneWidget);
|
expect(
|
||||||
|
find.byKey(const ValueKey('exercise-image-image-2')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('le chrono score démarre et s’arrête avec la bonne durée', (
|
testWidgets('le chrono score démarre et s’arrête avec la bonne durée', (
|
||||||
@ -282,6 +343,16 @@ void main() {
|
|||||||
|
|
||||||
await tester.tap(find.text('Démarrer'));
|
await tester.tap(find.text('Démarrer'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 0, 100);
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(find.text('00:00.1'), findsOneWidget);
|
||||||
|
|
||||||
|
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 0, 900);
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(find.text('00:00.9'), findsOneWidget);
|
||||||
|
|
||||||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
|
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
|
||||||
await tester.pump(const Duration(seconds: 1));
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
@ -843,6 +914,16 @@ Finder _textFieldWithLabel(String label) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<File> _createTestImageFile() async {
|
||||||
|
final directory = await Directory.systemTemp.createTemp('gametime-media-');
|
||||||
|
final file = File('${directory.path}/image.png');
|
||||||
|
const transparentPng =
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ'
|
||||||
|
'AAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||||
|
await file.writeAsBytes(base64Decode(transparentPng));
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
ActiveWorkoutSessionUseCases _activeUseCases(
|
ActiveWorkoutSessionUseCases _activeUseCases(
|
||||||
_FakeActiveSessionRepository repository,
|
_FakeActiveSessionRepository repository,
|
||||||
_FakeClock clock,
|
_FakeClock clock,
|
||||||
|
|||||||
Reference in New Issue
Block a user