From c0b1231bec25d8aad44103e446725c0e8efde466 Mon Sep 17 00:00:00 2001 From: Gustl22 Date: Sat, 9 Mar 2024 13:21:05 +0100 Subject: [PATCH] feat: StackTrace for ExceptionWidget --- .../lib/l10n/app_de.arb | 1 + .../lib/l10n/app_en.arb | 1 + .../team_match/team_match_overview.dart | 1 + .../lib/view/widgets/consumer.dart | 4 +- .../lib/view/widgets/dialogs.dart | 20 ++---- .../lib/view/widgets/exception.dart | 65 ++++++++++++++----- .../lib/view/widgets/loading_builder.dart | 4 +- 7 files changed, 60 insertions(+), 36 deletions(-) diff --git a/wrestling_scoreboard_client/lib/l10n/app_de.arb b/wrestling_scoreboard_client/lib/l10n/app_de.arb index b2ec47bd..96804f0b 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_de.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_de.arb @@ -58,6 +58,7 @@ "mandatoryField": "Das ist ein Pflichtfeld.", "actionSuccessful": "Die Aktion war erfolgreich.", "noWebSocketConnection": "Die Verbindung zum Server konnte nicht aufgebaut werden oder wurde unterbrochen.", + "errorOccurred": "Etwas ist schief gelaufen :/", "notFoundException": "Element wurde nicht gefunden :/", "invalidParameterException": "Die Änderung war nicht erfolgreich, bitte überprüfe deine Eingabeparameter.", "warningBoutGenerate": "Diese Aktion überschreibt alle existierenden Kämpfe dieser Begegnung. Um einzelne Kämpfe zu bearbeiten, nutze die Seite zur Kampf-Bearbeitung. Bist du sicher, dass du fortfahren möchtest?", diff --git a/wrestling_scoreboard_client/lib/l10n/app_en.arb b/wrestling_scoreboard_client/lib/l10n/app_en.arb index 30b61f22..791347c7 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_en.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_en.arb @@ -61,6 +61,7 @@ "mandatoryField": "This is a mandatory field.", "actionSuccessful": "The action was successful.", "noWebSocketConnection": "The connection to the server could not be established or was interrupted.", + "errorOccurred": "Something went wrong :/", "notFoundException": "Element was not found :/", "invalidParameterException": "The change was not successful, please check your input parameters.", "warningBoutGenerate": "This action overrides all existing bouts of this match. To edit single bouts, use the bout editing page. Are you sure, you want to continue?", diff --git a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart index 67ae0324..f821d24c 100644 --- a/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart +++ b/wrestling_scoreboard_client/lib/view/screens/overview/team_match/team_match_overview.dart @@ -80,6 +80,7 @@ class TeamMatchOverview extends ConsumerWidget { showExceptionDialog( context: context, exception: Exception('Please select a report provider in the settings'), + stackTrace: null, ); } } diff --git a/wrestling_scoreboard_client/lib/view/widgets/consumer.dart b/wrestling_scoreboard_client/lib/view/widgets/consumer.dart index 928be452..0bfea61e 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/consumer.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/consumer.dart @@ -55,12 +55,12 @@ class SingleConsumer extends StatelessWidget { @override Widget build(BuildContext context) { if (id == null && initialData == null) { - return ExceptionWidget(AppLocalizations.of(context)!.notFoundException); + return ExceptionWidget(AppLocalizations.of(context)!.notFoundException, stackTrace: null); } return NullableSingleConsumer( builder: (BuildContext context, T? data) { if (data == null) { - return ExceptionWidget(AppLocalizations.of(context)!.notFoundException); + return ExceptionWidget(AppLocalizations.of(context)!.notFoundException, stackTrace: null); } return builder(context, data); }, diff --git a/wrestling_scoreboard_client/lib/view/widgets/dialogs.dart b/wrestling_scoreboard_client/lib/view/widgets/dialogs.dart index 500965b2..d3957561 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/dialogs.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/dialogs.dart @@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:wrestling_scoreboard_client/services/network/remote/rest.dart'; 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; @@ -80,29 +81,20 @@ Future showOkCanelDialog({ ); } -Future showExceptionDialog({required BuildContext context, required Object exception}) async { - final localizations = AppLocalizations.of(context)!; +Future showExceptionDialog( + {required BuildContext context, required Object exception, required StackTrace? stackTrace}) async { await showOkDialog( context: context, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (exception is RestException) SelectableText(localizations.invalidParameterException), - SelectableText( - exception.toString(), - style: TextStyle(color: Theme.of(context).disabledColor, fontSize: 10), - ), - ], - ), + child: ExceptionInfo(exception, stackTrace: stackTrace), ); } Future catchAsync(BuildContext context, Future Function() doAsync) async { try { await doAsync(); - } catch (exception) { + } catch (exception, stackTrace) { if (context.mounted) { - showExceptionDialog(context: context, exception: exception); + showExceptionDialog(context: context, exception: exception, stackTrace: stackTrace); } } } diff --git a/wrestling_scoreboard_client/lib/view/widgets/exception.dart b/wrestling_scoreboard_client/lib/view/widgets/exception.dart index b92699c4..3f56425b 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/exception.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/exception.dart @@ -2,34 +2,63 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:wrestling_scoreboard_client/services/network/remote/rest.dart'; import 'package:wrestling_scoreboard_client/view/widgets/card.dart'; class ExceptionWidget extends StatelessWidget { final Object exception; + final StackTrace? stackTrace; final Function()? onRetry; - const ExceptionWidget(this.exception, {this.onRetry, super.key}); + const ExceptionWidget(this.exception, {this.onRetry, super.key, required this.stackTrace}); @override Widget build(BuildContext context) { - final errorText = exception is SocketException - ? SelectableText( - AppLocalizations.of(context)!.noWebSocketConnection, - ) - : SelectableText(exception.toString(), style: TextStyle(color: Theme.of(context).colorScheme.error)); return Center( - child: PaddedCard( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - errorText, - if (onRetry != null) const SizedBox(height: 16), - if (onRetry != null) OutlinedButton(onPressed: onRetry, child: Text(AppLocalizations.of(context)!.retry)) - ], - ), - ), + child: PaddedCard(child: ExceptionInfo(exception, stackTrace: stackTrace, onRetry: onRetry)), + ); + } +} + +class ExceptionInfo extends StatelessWidget { + final Object exception; + final StackTrace? stackTrace; + final Function()? onRetry; + + const ExceptionInfo(this.exception, {this.onRetry, super.key, required this.stackTrace}); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + String title; + if (exception is RestException) { + title = localizations.invalidParameterException; + } else if (exception is SocketException) { + title = localizations.noWebSocketConnection; + } else { + title = localizations.errorOccurred; + } + final disabledColor = Theme.of(context).disabledColor; + + final expansionTile = ExpansionTile( + title: Text(title), + childrenPadding: const EdgeInsets.all(16), + children: [ + SelectableText(exception.toString(), style: TextStyle(color: disabledColor, fontSize: 14)), + if (stackTrace != null) + SelectableText(stackTrace!.toString(), style: TextStyle(color: disabledColor, fontSize: 11)), + ], + ); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + expansionTile, + if (onRetry != null) const SizedBox(height: 16), + if (onRetry != null) OutlinedButton(onPressed: onRetry, child: Text(AppLocalizations.of(context)!.retry)) + ], ); } } diff --git a/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart b/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart index a7890005..d8d6d7bb 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/loading_builder.dart @@ -23,7 +23,7 @@ class LoadingBuilder extends ConsumerWidget { future: ref.read(networkTimeoutNotifierProvider).then((timeout) => future.timeout(timeout)), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { - return ExceptionWidget(snapshot.error!, onRetry: onRetry); + return ExceptionWidget(snapshot.error!, stackTrace: snapshot.stackTrace, onRetry: onRetry); } if (initialData != null) { return builder(context, initialData as T); @@ -58,7 +58,7 @@ class LoadingStreamBuilder extends StatelessWidget { initialData: initialData, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { - return ExceptionWidget(snapshot.error!, onRetry: onRetry); + return ExceptionWidget(snapshot.error!, onRetry: onRetry, stackTrace: snapshot.stackTrace); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator());