diff --git a/wrestling_scoreboard_client/lib/view/screens/home/home.dart b/wrestling_scoreboard_client/lib/view/screens/home/home.dart index 940cfa7d..8522e54a 100644 --- a/wrestling_scoreboard_client/lib/view/screens/home/home.dart +++ b/wrestling_scoreboard_client/lib/view/screens/home/home.dart @@ -24,6 +24,8 @@ import 'package:wrestling_scoreboard_client/view/screens/overview/team_match/tea import 'package:wrestling_scoreboard_client/view/screens/overview/team_overview.dart'; import 'package:wrestling_scoreboard_client/view/screens/overview/weight_class_overview.dart'; import 'package:wrestling_scoreboard_client/view/widgets/consumer.dart'; +import 'package:wrestling_scoreboard_client/view/widgets/dialogs.dart'; +import 'package:wrestling_scoreboard_client/view/widgets/exception.dart'; import 'package:wrestling_scoreboard_client/view/widgets/loading_builder.dart'; import 'package:wrestling_scoreboard_client/view/widgets/responsive_container.dart'; import 'package:wrestling_scoreboard_client/view/widgets/scaffold.dart'; @@ -130,7 +132,34 @@ class HomeState extends ConsumerState { } Widget _createItem(int id, String route, String Function(T dataObject) getTitle) { + final localizations = AppLocalizations.of(context)!; return SingleConsumer( + onException: (context, exception, {stackTrace}) => Card( + child: Center( + child: IconButton( + onPressed: () async { + final removeItem = await showOkCancelDialog( + okText: localizations.remove, + getResult: () => true, + context: context, + child: Column( + children: [ + Text('There was a problem with the object of type $T and id $id.'), + ExceptionInfo( + exception ?? localizations.errorOccurred, + stackTrace: stackTrace, + ), + ], + ), + ); + if (removeItem == true) { + final notifier = ref.read(favoritesNotifierProvider.notifier); + notifier.removeFavorite(getTableNameFromType(T), id); + } + }, + icon: const Icon(Icons.warning)), + ), + ), id: id, builder: (context, data) { return InkWell( diff --git a/wrestling_scoreboard_client/lib/view/screens/more/about.dart b/wrestling_scoreboard_client/lib/view/screens/more/about.dart index 76c6717c..1988ca2c 100644 --- a/wrestling_scoreboard_client/lib/view/screens/more/about.dart +++ b/wrestling_scoreboard_client/lib/view/screens/more/about.dart @@ -6,6 +6,7 @@ import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; import 'package:wrestling_scoreboard_client/main.dart'; import 'package:wrestling_scoreboard_client/view/widgets/card.dart'; +import 'package:wrestling_scoreboard_client/view/widgets/dialogs.dart'; import 'package:wrestling_scoreboard_client/view/widgets/loading_builder.dart'; import 'package:wrestling_scoreboard_client/view/widgets/responsive_container.dart'; import 'package:wrestling_scoreboard_client/view/widgets/scaffold.dart'; @@ -14,6 +15,7 @@ const loadCompleteChangelog = false; class AboutScreen extends StatelessWidget { static const route = 'about'; + const AboutScreen({super.key}); Future _loadChangelog() async { @@ -60,29 +62,21 @@ class AboutScreen extends StatelessWidget { ListTile( leading: const Icon(Icons.list), title: Text(localizations.about_Changelog), - onTap: () => showDialog( + onTap: () => showOkDialog( context: context, - builder: (BuildContext context) => AlertDialog( - actions: [ - TextButton( - child: Text(localizations.ok), - onPressed: () => Navigator.of(context).pop(), - ), - ], - content: LoadingBuilder( - future: _loadChangelog(), - builder: (context, data) { - return SingleChildScrollView( - child: MarkdownBody( - listItemCrossAxisAlignment: MarkdownListItemCrossAxisAlignment.start, - shrinkWrap: true, - selectable: true, - data: data, - onTapLink: (text, href, title) => launchUrl(Uri.parse(href!)), - ), - ); - }, - ), + child: LoadingBuilder( + future: _loadChangelog(), + builder: (context, data) { + return SingleChildScrollView( + child: MarkdownBody( + listItemCrossAxisAlignment: MarkdownListItemCrossAxisAlignment.start, + shrinkWrap: true, + selectable: true, + data: data, + onTapLink: (text, href, title) => launchUrl(Uri.parse(href!)), + ), + ); + }, ), ), ), diff --git a/wrestling_scoreboard_client/lib/view/widgets/consumer.dart b/wrestling_scoreboard_client/lib/view/widgets/consumer.dart index 8ea38358..e6bd31ba 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/consumer.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/consumer.dart @@ -12,9 +12,11 @@ class NullableSingleConsumer extends ConsumerWidget { final T? initialData; final int? id; final Widget Function(BuildContext context, T? data) builder; + final Widget Function(BuildContext context, Object? exception, {StackTrace? stackTrace})? onException; const NullableSingleConsumer({ required this.builder, + this.onException, required this.id, this.initialData, super.key, @@ -30,11 +32,13 @@ class NullableSingleConsumer extends ConsumerWidget { return LoadingBuilder( builder: builder, future: stream, - initialData: null, // Handle initial data via the stream + initialData: null, + // Handle initial data via the stream onRetry: () async => (await ref.read(webSocketManagerNotifierProvider)) .onWebSocketConnection .sink .add(WebSocketConnectionState.connecting), + onException: onException, ); } } @@ -43,28 +47,34 @@ class SingleConsumer extends StatelessWidget { final T? initialData; final int? id; final Widget Function(BuildContext context, T data) builder; + final Widget Function(BuildContext context, Object? exception, {StackTrace? stackTrace})? onException; const SingleConsumer({ required this.builder, required this.id, this.initialData, super.key, + this.onException, }); @override Widget build(BuildContext context) { if (id == null && initialData == null) { - return ExceptionCard(AppLocalizations.of(context)!.notFoundException, stackTrace: null); + return onException?.call(context, null) ?? + ExceptionCard(AppLocalizations.of(context)!.notFoundException, stackTrace: null); } return NullableSingleConsumer( - builder: (BuildContext context, T? data) { - if (data == null) { - return ExceptionCard(AppLocalizations.of(context)!.notFoundException, stackTrace: null); - } - return builder(context, data); - }, - id: id, - initialData: initialData); + builder: (BuildContext context, T? data) { + if (data == null) { + return onException?.call(context, null) ?? + ExceptionCard(AppLocalizations.of(context)!.notFoundException, stackTrace: null); + } + return builder(context, data); + }, + id: id, + initialData: initialData, + onException: onException, + ); } } diff --git a/wrestling_scoreboard_client/lib/view/widgets/dialogs.dart b/wrestling_scoreboard_client/lib/view/widgets/dialogs.dart index 0d7089bc..5c4c497b 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/dialogs.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/dialogs.dart @@ -5,25 +5,41 @@ import 'package:wrestling_scoreboard_client/view/utils.dart'; import 'package:wrestling_scoreboard_client/view/widgets/duration_picker.dart'; import 'package:wrestling_scoreboard_client/view/widgets/exception.dart'; -class OkDialog extends StatelessWidget { - final Widget child; +class SizedDialog extends StatelessWidget { + /// Do not wrap this into a column with shrinkwrap, so that ListViews act dynamically. + const SizedDialog({super.key, required this.actions, required this.child}); - const OkDialog({required this.child, super.key}); + final List actions; + final Widget child; @override Widget build(BuildContext context) { - final localizations = AppLocalizations.of(context)!; return AlertDialog( content: SizedBox( width: 300, child: SingleChildScrollView(child: child), ), - actions: [ + actions: actions, + ); + } +} + +class OkDialog extends StatelessWidget { + final Widget child; + + const OkDialog({required this.child, super.key}); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + return SizedDialog( + actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(localizations.ok), ), ], + child: child, ); } } @@ -47,12 +63,7 @@ class OkCancelDialog extends StatelessWidget { @override Widget build(BuildContext context) { final localizations = AppLocalizations.of(context)!; - return AlertDialog( - // Do not wrap this into a column with shrinkwrap, so that ListViews act dynamically. - content: SizedBox( - width: 300, - child: SingleChildScrollView(child: child), - ), + return SizedDialog( actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -63,6 +74,7 @@ class OkCancelDialog extends StatelessWidget { child: Text(okText ?? localizations.ok), ), ], + child: child, ); } } diff --git a/wrestling_scoreboard_client/lib/view/widgets/info.dart b/wrestling_scoreboard_client/lib/view/widgets/info.dart index 90b957d9..0b27a5e1 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/info.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/info.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:wrestling_scoreboard_client/view/widgets/dialogs.dart'; import 'package:wrestling_scoreboard_client/view/widgets/grouped_list.dart'; import 'package:wrestling_scoreboard_common/common.dart'; @@ -40,26 +41,17 @@ class InfoWidget extends StatelessWidget { ), IconButton( icon: const Icon(Icons.delete), - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - content: Text('${localizations.remove} $classLocale?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(localizations.cancel), - ), - TextButton( - onPressed: () { - onDelete(); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - child: Text(localizations.ok), - ), - ], - ), - ), + onPressed: () async { + final confirmDelete = await showOkCancelDialog( + context: context, + child: Text('${localizations.remove} $classLocale?'), + getResult: () => true, + ); + if (confirmDelete == true && context.mounted) { + Navigator.of(context).pop(); + onDelete(); + } + }, ), ], ), diff --git a/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart b/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart index bb727a00..8324966c 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart @@ -8,11 +8,13 @@ class LoadingBuilder extends ConsumerWidget { final T? initialData; final void Function()? onRetry; final Widget Function(BuildContext context, T data) builder; + final Widget Function(BuildContext context, Object? exception, {StackTrace? stackTrace})? onException; const LoadingBuilder({ super.key, required this.future, required this.builder, + this.onException, this.initialData, this.onRetry, }); @@ -23,7 +25,8 @@ class LoadingBuilder extends ConsumerWidget { future: ref.read(networkTimeoutNotifierProvider).then((timeout) => future.timeout(timeout)), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { - return ExceptionCard(snapshot.error!, stackTrace: snapshot.stackTrace, onRetry: onRetry); + return onException?.call(context, snapshot.error!, stackTrace: snapshot.stackTrace) ?? + ExceptionCard(snapshot.error!, stackTrace: snapshot.stackTrace, onRetry: onRetry); } if (initialData != null) { return builder(context, initialData as T);