From f6a073766abd6fc36f83cbceac5c5b8a90174ed3 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Mon, 10 Nov 2025 20:23:52 +0100 Subject: [PATCH] Improve and consolidate the image error handler --- lib/widgets/core/image.dart | 61 +++++++++++++++++++++++++++++-- lib/widgets/exercises/images.dart | 14 +++---- lib/widgets/gallery/overview.dart | 39 ++++++-------------- 3 files changed, 75 insertions(+), 39 deletions(-) diff --git a/lib/widgets/core/image.dart b/lib/widgets/core/image.dart index 49466b58..42b175dc 100644 --- a/lib/widgets/core/image.dart +++ b/lib/widgets/core/image.dart @@ -1,20 +1,73 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; -class ImageFormatNotSupported extends StatelessWidget { +Widget handleImageError( + BuildContext context, + Object error, + StackTrace? stackTrace, + String imageUrl, +) { + final imageFormat = imageUrl.split('.').last.toUpperCase(); + final logger = Logger('handleImageError'); + logger.warning('Failed to load image $imageUrl: $error, $stackTrace'); + + // NOTE: for the moment the other error messages are not localized + String message = ''; + switch (error.runtimeType) { + case NetworkImageLoadException: + message = 'Network error'; + case HttpException: + message = 'Http error'; + case FormatException: + //TODO: not sure if this is the right exception for unsupported image formats? + message = AppLocalizations.of(context).imageFormatNotSupported(imageFormat); + default: + message = 'Other exception'; + } + + return AspectRatio( + aspectRatio: 1, + child: ImageError( + message, + errorMessage: error.toString(), + ), + ); +} + +class ImageError extends StatelessWidget { final String title; + final String? errorMessage; - const ImageFormatNotSupported(this.title, {super.key}); + const ImageError(this.title, {this.errorMessage, super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( + padding: const EdgeInsets.all(5), color: theme.colorScheme.errorContainer, - child: Row( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, spacing: 8, - children: [const Icon(Icons.broken_image), Text(title)], + children: [ + if (errorMessage != null) + Tooltip(message: errorMessage, child: const Icon(Icons.broken_image)) + else + const Icon(Icons.broken_image), + + Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 2, + textAlign: TextAlign.center, + ), + ], ), ); } diff --git a/lib/widgets/exercises/images.dart b/lib/widgets/exercises/images.dart index 66a54686..595586b5 100644 --- a/lib/widgets/exercises/images.dart +++ b/lib/widgets/exercises/images.dart @@ -37,14 +37,12 @@ class ExerciseImageWidget extends StatelessWidget { ? Image.network( image!.url, semanticLabel: 'Exercise image', - errorBuilder: (context, error, stackTrace) { - _logger.warning('Failed to load image ${image!.url}: $error, $stackTrace'); - final imageFormat = image!.url.split('.').last.toUpperCase(); - - return ImageFormatNotSupported( - i18n.imageFormatNotSupported(imageFormat), - ); - }, + errorBuilder: (context, error, stackTrace) => handleImageError( + context, + error, + stackTrace, + image!.url, + ), ) : const Image( image: AssetImage('assets/images/placeholder.png'), diff --git a/lib/widgets/gallery/overview.dart b/lib/widgets/gallery/overview.dart index 0900ffa5..01fd229f 100644 --- a/lib/widgets/gallery/overview.dart +++ b/lib/widgets/gallery/overview.dart @@ -36,8 +36,6 @@ class Gallery extends StatelessWidget { @override Widget build(BuildContext context) { final provider = Provider.of(context); - final i18n = AppLocalizations.of(context); - final theme = Theme.of(context); return Padding( padding: const EdgeInsets.all(5), @@ -66,23 +64,12 @@ class Gallery extends StatelessWidget { image: NetworkImage(currentImage.url!), fit: BoxFit.cover, imageSemanticLabel: currentImage.description, - imageErrorBuilder: (context, error, stackTrace) { - final imageFormat = currentImage.url!.split('.').last.toUpperCase(); - return AspectRatio( - aspectRatio: 1, - child: Container( - color: theme.colorScheme.errorContainer, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8, - children: [ - const Icon(Icons.broken_image), - Text(i18n.imageFormatNotSupported(imageFormat)), - ], - ), - ), - ); - }, + imageErrorBuilder: (context, error, stackTrace) => handleImageError( + context, + error, + stackTrace, + currentImage.url!, + ), ), ); }, @@ -102,7 +89,6 @@ class ImageDetail extends StatelessWidget { @override Widget build(BuildContext context) { - final i18n = AppLocalizations.of(context); return Container( key: Key('image-${image.id!}-detail'), padding: const EdgeInsets.all(10), @@ -116,13 +102,12 @@ class ImageDetail extends StatelessWidget { child: Image.network( image.url!, semanticLabel: image.description, - errorBuilder: (context, error, stackTrace) { - final imageFormat = image.url!.split('.').last.toUpperCase(); - - return ImageFormatNotSupported( - i18n.imageFormatNotSupported(imageFormat), - ); - }, + errorBuilder: (context, error, stackTrace) => handleImageError( + context, + error, + stackTrace, + image.url!, + ), ), ), Padding(