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;
|
||||
}
|
||||
|
||||
Future<MediaAsset?> findById(String id) {
|
||||
return mediaRepository.findById(id);
|
||||
}
|
||||
|
||||
Future<Exercise> attachToExercise({
|
||||
required String exerciseId,
|
||||
required String mediaAssetId,
|
||||
|
||||
@ -13,6 +13,7 @@ final class HistoryListScreen extends StatefulWidget {
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.activeUseCases,
|
||||
required this.closeUseCase,
|
||||
this.mediaUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -20,6 +21,7 @@ final class HistoryListScreen extends StatefulWidget {
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final MediaUseCases? mediaUseCases;
|
||||
|
||||
@override
|
||||
State<HistoryListScreen> createState() => _HistoryListScreenState();
|
||||
@ -93,6 +95,7 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
|
||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||
activeUseCases: widget.activeUseCases,
|
||||
closeUseCase: widget.closeUseCase,
|
||||
mediaUseCases: widget.mediaUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -111,6 +114,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.activeUseCases,
|
||||
required this.closeUseCase,
|
||||
this.mediaUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -119,6 +123,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final MediaUseCases? mediaUseCases;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -191,6 +196,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
||||
closeUseCase: closeUseCase,
|
||||
historyUseCases: historyUseCases,
|
||||
workoutTemplateUseCases: workoutTemplateUseCases,
|
||||
mediaUseCases: mediaUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -154,6 +154,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -170,6 +171,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
widget.bootstrap.workoutTemplateUseCases,
|
||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -188,6 +190,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
@ -10,6 +12,8 @@ import 'theme.dart';
|
||||
|
||||
enum WorkoutExecutionMode { active, rest, paused, finished }
|
||||
|
||||
typedef VideoMediaBuilder = Widget Function(BuildContext context, MediaAsset asset);
|
||||
|
||||
final class WorkoutExecutionScreen extends StatefulWidget {
|
||||
const WorkoutExecutionScreen({
|
||||
required this.initialSession,
|
||||
@ -17,6 +21,9 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
||||
required this.closeUseCase,
|
||||
required this.historyUseCases,
|
||||
required this.workoutTemplateUseCases,
|
||||
this.mediaUseCases,
|
||||
this.mediaAssetLoader,
|
||||
this.videoMediaBuilder,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -25,6 +32,9 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final WorkoutHistoryUseCases historyUseCases;
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
final MediaUseCases? mediaUseCases;
|
||||
final Future<MediaAsset?> Function(String id)? mediaAssetLoader;
|
||||
final VideoMediaBuilder? videoMediaBuilder;
|
||||
|
||||
@override
|
||||
State<WorkoutExecutionScreen> createState() => _WorkoutExecutionScreenState();
|
||||
@ -36,6 +46,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
late WorkoutExecutionMode _mode;
|
||||
Timer? _ticker;
|
||||
Timer? _restTicker;
|
||||
Timer? _scoreStopwatchTicker;
|
||||
WorkoutHistory? _completedHistory;
|
||||
String? _activeRestStateId;
|
||||
ActiveScoreStopwatchState? _scoreStopwatch;
|
||||
@ -73,6 +84,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
void dispose() {
|
||||
_ticker?.cancel();
|
||||
_restTicker?.cancel();
|
||||
_scoreStopwatchTicker?.cancel();
|
||||
_scoreController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@ -339,10 +351,20 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
restRemainingSeconds: _mode == WorkoutExecutionMode.rest
|
||||
? _remainingRestSeconds
|
||||
: 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 {
|
||||
final manual = _manualScoreTimeMs;
|
||||
if (manual != null) {
|
||||
@ -359,6 +381,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
if (!_exercise.stopwatchScoreEnabled) {
|
||||
_scoreStopwatch = null;
|
||||
_manualScoreTimeMs = null;
|
||||
_refreshScoreStopwatchTicker();
|
||||
return;
|
||||
}
|
||||
final state = await widget.activeUseCases.findScoreStopwatch(
|
||||
@ -369,6 +392,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _scoreStopwatch = state);
|
||||
_refreshScoreStopwatchTicker();
|
||||
}
|
||||
|
||||
Future<void> _startScoreStopwatch() async {
|
||||
@ -383,6 +407,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
_scoreStopwatch = state;
|
||||
_manualScoreTimeMs = null;
|
||||
});
|
||||
_refreshScoreStopwatchTicker();
|
||||
}
|
||||
|
||||
Future<ActiveScoreStopwatchState?> _stopScoreStopwatch() async {
|
||||
@ -396,6 +421,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
);
|
||||
if (!mounted) return stopped;
|
||||
setState(() => _scoreStopwatch = stopped);
|
||||
_refreshScoreStopwatchTicker();
|
||||
return stopped;
|
||||
}
|
||||
|
||||
@ -411,6 +437,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
_scoreStopwatch = state;
|
||||
_manualScoreTimeMs = null;
|
||||
});
|
||||
_refreshScoreStopwatchTicker();
|
||||
}
|
||||
|
||||
Future<void> _resetScoreStopwatch() async {
|
||||
@ -445,6 +472,23 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
_scoreStopwatch = 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 {
|
||||
@ -575,6 +619,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
_scoreStopwatch = null;
|
||||
_manualScoreTimeMs = null;
|
||||
});
|
||||
_refreshScoreStopwatchTicker();
|
||||
}
|
||||
if (!skipped &&
|
||||
_exercise.stopwatchScoreEnabled &&
|
||||
@ -767,6 +812,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
_activeRestStateId = null;
|
||||
_scoreStopwatch = null;
|
||||
_manualScoreTimeMs = null;
|
||||
_refreshScoreStopwatchTicker();
|
||||
_seriesStartedAt = DateTime.now().toUtc();
|
||||
_session = await widget.activeUseCases.updateProgress(
|
||||
sessionId: _session.metadata.id,
|
||||
@ -836,6 +882,9 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
closeUseCase: widget.closeUseCase,
|
||||
historyUseCases: widget.historyUseCases,
|
||||
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 {
|
||||
const _ExerciseMediaSheet({
|
||||
required this.exercise,
|
||||
required this.mediaAssetLoader,
|
||||
this.videoMediaBuilder,
|
||||
this.restRemainingSeconds,
|
||||
});
|
||||
|
||||
final ExecutionExercise exercise;
|
||||
final Future<MediaAsset?> Function(String id) mediaAssetLoader;
|
||||
final VideoMediaBuilder? videoMediaBuilder;
|
||||
final int? restRemainingSeconds;
|
||||
|
||||
@override
|
||||
@ -1346,9 +1399,14 @@ final class _ExerciseMediaSheetState extends State<_ExerciseMediaSheet> {
|
||||
pageController: _pageController,
|
||||
currentIndex: _imageIndex,
|
||||
onPageChanged: (index) => setState(() => _imageIndex = index),
|
||||
mediaAssetLoader: widget.mediaAssetLoader,
|
||||
)
|
||||
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.currentIndex,
|
||||
required this.onPageChanged,
|
||||
required this.mediaAssetLoader,
|
||||
});
|
||||
|
||||
final List<String> imageMediaIds;
|
||||
final PageController pageController;
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onPageChanged;
|
||||
final Future<MediaAsset?> Function(String id) mediaAssetLoader;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -1396,6 +1456,7 @@ final class _ImageMediaGallery extends StatelessWidget {
|
||||
return _ImageMediaPage(
|
||||
mediaAssetId: imageMediaIds[index],
|
||||
index: index,
|
||||
mediaAssetLoader: mediaAssetLoader,
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -1407,10 +1468,15 @@ final class _ImageMediaGallery 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 int index;
|
||||
final Future<MediaAsset?> Function(String id) mediaAssetLoader;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -1422,55 +1488,201 @@ final class _ImageMediaPage extends StatelessWidget {
|
||||
border: Border.all(color: tokens.border),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.image_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
child: FutureBuilder<MediaAsset?>(
|
||||
future: mediaAssetLoader(mediaAssetId),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
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 {
|
||||
const _VideoMediaPanel({required this.videoMediaId});
|
||||
const _VideoMediaPanel({
|
||||
required this.videoMediaId,
|
||||
required this.mediaAssetLoader,
|
||||
this.videoMediaBuilder,
|
||||
});
|
||||
|
||||
final String videoMediaId;
|
||||
final Future<MediaAsset?> Function(String id) mediaAssetLoader;
|
||||
final VideoMediaBuilder? videoMediaBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CourtBlazerAccentPanel(
|
||||
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,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Vidéo', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 4),
|
||||
Text(videoMediaId, style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
final asset = snapshot.data;
|
||||
if (asset == null || asset.kind != MediaKind.video) {
|
||||
return SizedBox(
|
||||
height: 260,
|
||||
child: _MissingMediaPlaceholder(
|
||||
icon: Icons.videocam_off_outlined,
|
||||
title: 'Vidéo indisponible',
|
||||
subtitle: videoMediaId,
|
||||
),
|
||||
);
|
||||
}
|
||||
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 {
|
||||
|
||||
@ -13,6 +13,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
required this.activeUseCases,
|
||||
required this.closeUseCase,
|
||||
required this.historyUseCases,
|
||||
this.mediaUseCases,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -21,6 +22,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final WorkoutHistoryUseCases historyUseCases;
|
||||
final MediaUseCases? mediaUseCases;
|
||||
|
||||
@override
|
||||
State<WorkoutTemplateListScreen> createState() =>
|
||||
@ -141,6 +143,7 @@ final class _WorkoutTemplateListScreenState
|
||||
closeUseCase: widget.closeUseCase,
|
||||
historyUseCases: widget.historyUseCases,
|
||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||
mediaUseCases: widget.mediaUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
56
pubspec.lock
56
pubspec.lock
@ -177,6 +177,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -344,6 +352,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -829,6 +845,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -17,6 +17,7 @@ dependencies:
|
||||
path: ^1.9.1
|
||||
path_provider: ^2.1.5
|
||||
sqlite3_flutter_libs: ^0.6.0+eol
|
||||
video_player: ^2.11.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.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', (
|
||||
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 session = ActiveWorkoutSession(
|
||||
metadata: _metadata('session-1'),
|
||||
@ -170,6 +177,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: WorkoutExecutionScreen(
|
||||
@ -181,23 +189,62 @@ void main() {
|
||||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||||
historyUseCases: _historyUseCases(clock),
|
||||
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);
|
||||
|
||||
await tester.tap(find.text('Voir médias'));
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
expect(find.text('Médias de l’exercice'), findsOneWidget);
|
||||
expect(find.text('Images'), findsOneWidget);
|
||||
expect(find.text('Vidéo'), 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 {
|
||||
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 session = ActiveWorkoutSession(
|
||||
metadata: _metadata('session-1'),
|
||||
@ -214,6 +261,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: WorkoutExecutionScreen(
|
||||
@ -225,20 +273,33 @@ void main() {
|
||||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||||
historyUseCases: _historyUseCases(clock),
|
||||
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.runAsync(() async {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
expect(find.text('1/3'), findsOneWidget);
|
||||
|
||||
await tester.drag(find.byType(PageView), const Offset(-400, 0));
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
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', (
|
||||
@ -282,6 +343,16 @@ void main() {
|
||||
|
||||
await tester.tap(find.text('Démarrer'));
|
||||
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);
|
||||
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(
|
||||
_FakeActiveSessionRepository repository,
|
||||
_FakeClock clock,
|
||||
|
||||
Reference in New Issue
Block a user