From 1dc3776e7e111f83da999fd3ee9346e06d4c6a26 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 2 Sep 2025 14:49:28 +0200 Subject: [PATCH] Improve video error handling Users now see a smaller error message in the exercise description, instead of the big general popup. --- lib/helpers/errors.dart | 58 ++++++++++++++++++++++++++++--- lib/models/exercises/video.dart | 2 ++ lib/widgets/exercises/videos.dart | 52 +++++++++++++++++++-------- 3 files changed, 94 insertions(+), 18 deletions(-) diff --git a/lib/helpers/errors.dart b/lib/helpers/errors.dart index 91a4a75ee..e11326946 100644 --- a/lib/helpers/errors.dart +++ b/lib/helpers/errors.dart @@ -49,7 +49,7 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, {BuildContext? co return; } - final errorList = formatErrors(extractErrors(exception.errors)); + final errorList = formatApiErrors(extractErrors(exception.errors)); showDialog( context: dialogContext, @@ -104,6 +104,15 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext } else if (error is SocketException) { isNetworkError = true; } + /* + else if (error is PlatformException) { + errorTitle = 'Problem with media'; + errorMessage = + 'There was a problem loading the media. This can be a e.g. problem with the codec that' + 'is not supported by your device. Original error message: ${error.message}'; + } + + */ final String fullStackTrace = stackTrace?.toString() ?? 'No stack trace available.'; @@ -115,13 +124,13 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext builder: (BuildContext context) { return AlertDialog( title: Row( + spacing: 8, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isNetworkError ? Icons.signal_wifi_connected_no_internet_4_outlined : Icons.error, color: Theme.of(context).colorScheme.error, ), - const SizedBox(width: 8), Expanded( child: Text( isNetworkError ? i18n.errorCouldNotConnectToServer : i18n.anErrorOccurred, @@ -309,7 +318,7 @@ List extractErrors(Map errors) { } /// Processes the error messages from the server and returns a list of widgets -List formatErrors(List errors, {Color? color}) { +List formatApiErrors(List errors, {Color? color}) { final textColor = color ?? Colors.black; final List errorList = []; @@ -328,6 +337,26 @@ List formatErrors(List errors, {Color? color}) { return errorList; } +/// Processes the error messages from the server and returns a list of widgets +List formatTextErrors(List errors, {String? title, Color? color}) { + final textColor = color ?? Colors.black; + + final List errorList = []; + + if (title != null) { + errorList.add( + Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: textColor)), + ); + } + + for (final message in errors) { + errorList.add(Text(message, style: TextStyle(color: textColor))); + } + errorList.add(const SizedBox(height: 8)); + + return errorList; +} + class FormHttpErrorsWidget extends StatelessWidget { final WgerHttpException exception; @@ -338,7 +367,7 @@ class FormHttpErrorsWidget extends StatelessWidget { return Column( children: [ Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error), - ...formatErrors( + ...formatApiErrors( extractErrors(exception.errors), color: Theme.of(context).colorScheme.error, ), @@ -346,3 +375,24 @@ class FormHttpErrorsWidget extends StatelessWidget { ); } } + +class GeneralErrorsWidget extends StatelessWidget { + final String? title; + final List widgets; + + const GeneralErrorsWidget(this.widgets, {this.title, super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error), + ...formatTextErrors( + widgets, + title: title, + color: Theme.of(context).colorScheme.error, + ), + ], + ); + } +} diff --git a/lib/models/exercises/video.dart b/lib/models/exercises/video.dart index c499664d8..a6948baa0 100644 --- a/lib/models/exercises/video.dart +++ b/lib/models/exercises/video.dart @@ -32,6 +32,8 @@ class Video { @JsonKey(name: 'video', required: true) final String url; + Uri get uri => Uri.parse(url); + @JsonKey(name: 'exercise', required: true) final int exerciseId; diff --git a/lib/widgets/exercises/videos.dart b/lib/widgets/exercises/videos.dart index aad1ebd91..0a5496bf3 100644 --- a/lib/widgets/exercises/videos.dart +++ b/lib/widgets/exercises/videos.dart @@ -17,7 +17,10 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; import 'package:video_player/video_player.dart'; +import 'package:wger/helpers/errors.dart'; import 'package:wger/models/exercises/video.dart'; class ExerciseVideoWidget extends StatefulWidget { @@ -31,35 +34,56 @@ class ExerciseVideoWidget extends StatefulWidget { class _ExerciseVideoWidgetState extends State { late VideoPlayerController _controller; + bool hasError = false; + final logger = Logger('ExerciseVideoWidgetState'); @override void initState() { super.initState(); - _controller = VideoPlayerController.network(widget.video.url); - _controller.addListener(() { + _controller = VideoPlayerController.networkUrl(widget.video.uri); + _initializeVideo(); + } + + Future _initializeVideo() async { + try { + await _controller.initialize(); setState(() {}); - }); - _controller.initialize().then((_) => setState(() {})); + } on PlatformException catch (e) { + if (mounted) { + setState(() => hasError = true); + } + + logger.warning('PlatformException while initializing video: ${e.message}'); + } } @override void dispose() { - super.dispose(); _controller.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - return _controller.value.isInitialized - ? AspectRatio( - aspectRatio: _controller.value.aspectRatio, - child: Stack(alignment: Alignment.bottomCenter, children: [ - VideoPlayer(_controller), - _ControlsOverlay(controller: _controller), - VideoProgressIndicator(_controller, allowScrubbing: true), - ]), + return hasError + ? const GeneralErrorsWidget( + [ + 'An error happened while loading the video. If you can, please check the application logs.' + ], ) - : Container(); + : _controller.value.isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ) + : Container(); } }