diff --git a/packages/neon/neon/lib/l10n/en.arb b/packages/neon/neon/lib/l10n/en.arb index a5a715f9..5bd46a6c 100644 --- a/packages/neon/neon/lib/l10n/en.arb +++ b/packages/neon/neon/lib/l10n/en.arb @@ -58,7 +58,6 @@ } } }, - "errorUnableToOpenFile": "Unable to open the file", "errorUnsupportedAppVersions": "Sorry, the version of the following apps on your Nextcloud instance are not supported. \n {names} \n Please contact your administrator to resolve the issues.", "@errorUnsupportedAppVersions" : { "placeholders": { diff --git a/packages/neon/neon/lib/l10n/localizations.dart b/packages/neon/neon/lib/l10n/localizations.dart index f95d6212..d287e80a 100644 --- a/packages/neon/neon/lib/l10n/localizations.dart +++ b/packages/neon/neon/lib/l10n/localizations.dart @@ -239,12 +239,6 @@ abstract class AppLocalizations { /// **'Permission for {name} is missing'** String errorMissingPermission(String name); - /// No description provided for @errorUnableToOpenFile. - /// - /// In en, this message translates to: - /// **'Unable to open the file'** - String get errorUnableToOpenFile; - /// No description provided for @errorUnsupportedAppVersions. /// /// In en, this message translates to: diff --git a/packages/neon/neon/lib/l10n/localizations_en.dart b/packages/neon/neon/lib/l10n/localizations_en.dart index 071828f9..45a8ccc8 100644 --- a/packages/neon/neon/lib/l10n/localizations_en.dart +++ b/packages/neon/neon/lib/l10n/localizations_en.dart @@ -106,9 +106,6 @@ class AppLocalizationsEn extends AppLocalizations { return 'Permission for $name is missing'; } - @override - String get errorUnableToOpenFile => 'Unable to open the file'; - @override String errorUnsupportedAppVersions(String names) { return 'Sorry, the version of the following apps on your Nextcloud instance are not supported. \n $names \n Please contact your administrator to resolve the issues.'; diff --git a/packages/neon/neon/lib/src/pages/account_settings.dart b/packages/neon/neon/lib/src/pages/account_settings.dart index f6bbf4d8..bcb694e9 100644 --- a/packages/neon/neon/lib/src/pages/account_settings.dart +++ b/packages/neon/neon/lib/src/pages/account_settings.dart @@ -13,7 +13,7 @@ import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/settings/widgets/settings_list.dart'; import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/utils/confirmation_dialog.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:nextcloud/nextcloud.dart'; @@ -104,7 +104,7 @@ class AccountSettingsPage extends StatelessWidget { ), ), ], - NeonException( + NeonError( userDetails.error, onRetry: userDetailsBloc.refresh, ), diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 44c5c057..2ff64712 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -14,7 +14,7 @@ import 'package:neon/src/utils/global_popups.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/widgets/app_bar.dart'; import 'package:neon/src/widgets/drawer.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/unified_search_results.dart'; import 'package:provider/provider.dart'; @@ -91,7 +91,7 @@ class _HomePageState extends State { debugPrint(e.toString()); debugPrint(s.toString()); if (mounted) { - NeonException.showSnackbar(context, e); + NeonError.showSnackbar(context, e); } } } diff --git a/packages/neon/neon/lib/src/pages/login_check_account.dart b/packages/neon/neon/lib/src/pages/login_check_account.dart index 31b8f775..08a9d0bf 100644 --- a/packages/neon/neon/lib/src/pages/login_check_account.dart +++ b/packages/neon/neon/lib/src/pages/login_check_account.dart @@ -12,7 +12,7 @@ import 'package:neon/src/router.dart'; import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/widgets/account_tile.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/validation_tile.dart'; @internal @@ -68,11 +68,11 @@ class _LoginCheckAccountPageState extends State { if (state.hasError) ...[ Builder( builder: (final context) { - final details = NeonException.getDetails(context, state.error); + final details = NeonError.getDetails(state.error); return NeonValidationTile( title: details.isUnauthorized ? AppLocalizations.of(context).errorCredentialsForAccountNoLongerMatch - : details.text, + : details.getText(context), state: ValidationState.failure, ); }, @@ -91,7 +91,7 @@ class _LoginCheckAccountPageState extends State { const HomeRoute().go(context); } : () { - if (state.hasError && NeonException.getDetails(context, state.error).isUnauthorized) { + if (state.hasError && NeonError.getDetails(state.error).isUnauthorized) { Navigator.pop(context); return; } diff --git a/packages/neon/neon/lib/src/pages/login_check_server_status.dart b/packages/neon/neon/lib/src/pages/login_check_server_status.dart index aee497a2..c9af2a0c 100644 --- a/packages/neon/neon/lib/src/pages/login_check_server_status.dart +++ b/packages/neon/neon/lib/src/pages/login_check_server_status.dart @@ -6,7 +6,7 @@ import 'package:neon/src/bloc/result_builder.dart'; import 'package:neon/src/blocs/login_check_server_status.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/theme/dialog.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/validation_tile.dart'; import 'package:nextcloud/nextcloud.dart'; @@ -67,7 +67,7 @@ class _LoginCheckServerStatusPageState extends State children: [ if (state.hasError) ...[ NeonValidationTile( - title: NeonException.getDetails(context, state.error).text, + title: NeonError.getDetails(state.error).getText(context), state: ValidationState.failure, ), ], diff --git a/packages/neon/neon/lib/src/pages/login_flow.dart b/packages/neon/neon/lib/src/pages/login_flow.dart index 99fef108..e7144ae4 100644 --- a/packages/neon/neon/lib/src/pages/login_flow.dart +++ b/packages/neon/neon/lib/src/pages/login_flow.dart @@ -4,7 +4,7 @@ import 'package:neon/l10n/localizations.dart'; import 'package:neon/src/bloc/result_builder.dart'; import 'package:neon/src/blocs/login_flow.dart'; import 'package:neon/src/router.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -68,7 +68,7 @@ class _LoginFlowPageState extends State { NeonLinearProgressIndicator( visible: init.isLoading, ), - NeonException( + NeonError( init.error, onRetry: bloc.refresh, ), diff --git a/packages/neon/neon/lib/src/pages/login_qr_code.dart b/packages/neon/neon/lib/src/pages/login_qr_code.dart index 2446dbbc..d4293427 100644 --- a/packages/neon/neon/lib/src/pages/login_qr_code.dart +++ b/packages/neon/neon/lib/src/pages/login_qr_code.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_zxing/flutter_zxing.dart'; import 'package:meta/meta.dart'; +import 'package:neon/l10n/localizations.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/utils/exceptions.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; @internal class LoginQRcodePage extends StatefulWidget { @@ -35,11 +36,11 @@ class _LoginQRcodePageState extends State { try { url = code.text; if (url == null) { - throw InvalidQRcodeException(); + throw const InvalidQRcodeException(); } final match = LoginQRcode.tryParse(url); if (match == null) { - throw InvalidQRcodeException(); + throw const InvalidQRcodeException(); } LoginCheckServerStatusRoute.withCredentials( @@ -53,10 +54,20 @@ class _LoginQRcodePageState extends State { debugPrint(s.toString()); _lastErrorURL = url; - NeonException.showSnackbar(context, e); + NeonError.showSnackbar(context, e); } } }, ), ); } + +@immutable +class InvalidQRcodeException extends NeonException { + const InvalidQRcodeException(); + + @override + NeonExceptionDetails get details => NeonExceptionDetails( + getText: (final context) => AppLocalizations.of(context).errorInvalidQRcode, + ); +} diff --git a/packages/neon/neon/lib/src/pages/settings.dart b/packages/neon/neon/lib/src/pages/settings.dart index 878e8247..d4b4cd42 100644 --- a/packages/neon/neon/lib/src/pages/settings.dart +++ b/packages/neon/neon/lib/src/pages/settings.dart @@ -23,7 +23,7 @@ import 'package:neon/src/utils/confirmation_dialog.dart'; import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/save_file.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -287,7 +287,7 @@ class _SettingsPageState extends State { debugPrint(e.toString()); debugPrint(s.toString()); if (mounted) { - NeonException.showSnackbar(context, e); + NeonError.showSnackbar(context, e); } } }, @@ -312,7 +312,7 @@ class _SettingsPageState extends State { if (!result.files.single.path!.endsWith('.json')) { if (mounted) { - NeonException.showSnackbar( + NeonError.showSnackbar( context, AppLocalizations.of(context).settingsImportWrongFileExtension, ); @@ -325,7 +325,7 @@ class _SettingsPageState extends State { debugPrint(e.toString()); debugPrint(s.toString()); if (mounted) { - NeonException.showSnackbar(context, e); + NeonError.showSnackbar(context, e); } } }, diff --git a/packages/neon/neon/lib/src/platform/android.dart b/packages/neon/neon/lib/src/platform/android.dart index 253ec6bd..93c27089 100644 --- a/packages/neon/neon/lib/src/platform/android.dart +++ b/packages/neon/neon/lib/src/platform/android.dart @@ -31,7 +31,7 @@ class AndroidNeonPlatform implements NeonPlatform { @override Future get userAccessibleAppDataPath async { if (!await Permission.storage.request().isGranted) { - throw MissingPermissionException(Permission.storage); + throw const MissingPermissionException(Permission.storage); } return p.join((await getExternalStorageDirectory())!.path); diff --git a/packages/neon/neon/lib/src/utils/exceptions.dart b/packages/neon/neon/lib/src/utils/exceptions.dart index 394627e4..41732471 100644 --- a/packages/neon/neon/lib/src/utils/exceptions.dart +++ b/packages/neon/neon/lib/src/utils/exceptions.dart @@ -1,11 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:neon/l10n/localizations.dart'; +import 'package:neon/src/models/label_builder.dart'; import 'package:permission_handler/permission_handler.dart'; -class MissingPermissionException implements Exception { - MissingPermissionException(this.permission); +/// Details of a [NeonException]. +class NeonExceptionDetails { + /// Creates new [NeonExceptionDetails]. + /// + /// [isUnauthorized] defaults to false. + const NeonExceptionDetails({ + required this.getText, + this.isUnauthorized = false, + }); - final Permission permission; + /// Text that will be displayed in the UI + final LabelBuilder getText; + + /// If the [Exception] is the result of an unauthorized API request this should be set to `true`. + /// + /// The user will then be shown a button to update the credentials of the account instead of retrying the action. + final bool isUnauthorized; +} + +/// Extensible [Exception] to be used for displaying custom errors in the UI. +@immutable +abstract class NeonException implements Exception { + /// Creates a NeonException + const NeonException(); + + /// Details that will be rendered by the UI. + NeonExceptionDetails get details; } -class UnableToOpenFileException implements Exception {} +/// [Exception] that should be thrown when a native permission is denied. +class MissingPermissionException extends NeonException { + /// Creates a MissingPermissionException + const MissingPermissionException(this.permission); + + /// Permission that was denied + final Permission permission; -class InvalidQRcodeException implements Exception {} + @override + NeonExceptionDetails get details => NeonExceptionDetails( + getText: (final context) => + AppLocalizations.of(context).errorMissingPermission(permission.toString().split('.')[1]), + ); +} diff --git a/packages/neon/neon/lib/src/widgets/account_tile.dart b/packages/neon/neon/lib/src/widgets/account_tile.dart index 923e550d..559dc1b9 100644 --- a/packages/neon/neon/lib/src/widgets/account_tile.dart +++ b/packages/neon/neon/lib/src/widgets/account_tile.dart @@ -4,7 +4,7 @@ import 'package:neon/src/bloc/result_builder.dart'; import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/utils/provider.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/user_avatar.dart'; import 'package:nextcloud/nextcloud.dart'; @@ -79,7 +79,7 @@ class NeonAccountTile extends StatelessWidget { const SizedBox( width: 5, ), - NeonException( + NeonError( userDetails.error, onlyIcon: true, iconSize: 24, diff --git a/packages/neon/neon/lib/src/widgets/app_bar.dart b/packages/neon/neon/lib/src/widgets/app_bar.dart index 25408ba7..d2ece2c6 100644 --- a/packages/neon/neon/lib/src/widgets/app_bar.dart +++ b/packages/neon/neon/lib/src/widgets/app_bar.dart @@ -12,7 +12,7 @@ import 'package:neon/src/models/notifications_interface.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/widgets/account_switcher_button.dart'; import 'package:neon/src/widgets/app_implementation_icon.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; @@ -89,7 +89,7 @@ class _NeonAppBarState extends State { const SizedBox( width: 8, ), - NeonException( + NeonError( appImplementations.error, onRetry: appsBloc.refresh, onlyIcon: true, diff --git a/packages/neon/neon/lib/src/widgets/cached_image.dart b/packages/neon/neon/lib/src/widgets/cached_image.dart index 11de17d9..4f393d73 100644 --- a/packages/neon/neon/lib/src/widgets/cached_image.dart +++ b/packages/neon/neon/lib/src/widgets/cached_image.dart @@ -9,7 +9,7 @@ import 'package:neon/nextcloud.dart'; import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/utils/provider.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; typedef CacheReviver = FutureOr Function(CacheManager cacheManager); @@ -180,9 +180,9 @@ class _NeonCachedImageState extends State { ), ); - Widget _buildError(final dynamic error) => + Widget _buildError(final Object? error) => widget.errorBuilder?.call(context, error) ?? - NeonException( + NeonError( error, onRetry: () { setState(() {}); diff --git a/packages/neon/neon/lib/src/widgets/drawer.dart b/packages/neon/neon/lib/src/widgets/drawer.dart index 5fa594be..8eea8854 100644 --- a/packages/neon/neon/lib/src/widgets/drawer.dart +++ b/packages/neon/neon/lib/src/widgets/drawer.dart @@ -11,7 +11,7 @@ import 'package:neon/src/router.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/widgets/cached_image.dart'; import 'package:neon/src/widgets/drawer_destination.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:nextcloud/nextcloud.dart'; @@ -131,7 +131,7 @@ class NeonDrawerHeader extends StatelessWidget { } if (capabilities.hasError) { - return NeonException( + return NeonError( capabilities.error, onRetry: capabilitiesBloc.refresh, ); diff --git a/packages/neon/neon/lib/src/widgets/error.dart b/packages/neon/neon/lib/src/widgets/error.dart new file mode 100644 index 00000000..f0a9328d --- /dev/null +++ b/packages/neon/neon/lib/src/widgets/error.dart @@ -0,0 +1,191 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; +import 'package:meta/meta.dart'; +import 'package:neon/l10n/localizations.dart'; +import 'package:neon/src/blocs/accounts.dart'; +import 'package:neon/src/router.dart'; +import 'package:neon/src/utils/exceptions.dart'; +import 'package:neon/src/utils/provider.dart'; +import 'package:nextcloud/nextcloud.dart'; + +/// An indicator that an [error] has occurred. +/// +/// The action that lead to the error can be retried. +class NeonError extends StatelessWidget { + /// Creates a NeonError. + const NeonError( + this.error, { + required this.onRetry, + this.onlyIcon = false, + this.iconSize, + this.color, + super.key, + }); + + /// The error object. + /// + /// Can be of type [String] or [Exception], various subtypes of `Exception` are also handled separately. + final Object? error; + + /// A function that's called when the user decides to retry the action that lead to the error. + final VoidCallback onRetry; + + /// Changes whether the text is displayed additionally or not. + final bool onlyIcon; + + /// The size of the icon in logical pixels. + /// + /// Defaults to a size of `30`. + final double? iconSize; + + /// The color to use when drawing the error indicator. + /// + /// Defaults to the nearest [IconTheme]'s [ColorScheme.error]. + final Color? color; + + /// Shows a [SnackBar] popup for the [error]. + static void showSnackbar(final BuildContext context, final Object? error) { + final details = getDetails(error); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(details.getText(context)), + action: details.isUnauthorized + ? SnackBarAction( + label: AppLocalizations.of(context).loginAgain, + onPressed: () => _openLoginPage(context), + ) + : null, + ), + ); + } + + @override + Widget build(final BuildContext context) { + if (error == null) { + return const SizedBox(); + } + + final details = getDetails(error); + final color = this.color ?? Theme.of(context).colorScheme.error; + + final errorIcon = Icon( + Icons.error_outline, + size: iconSize ?? 30, + color: color, + ); + + final message = + details.isUnauthorized ? AppLocalizations.of(context).loginAgain : AppLocalizations.of(context).actionRetry; + + final onPressed = details.isUnauthorized ? () => _openLoginPage(context) : onRetry; + + if (onlyIcon) { + return Semantics( + tooltip: details.getText(context), + child: IconButton( + icon: errorIcon, + padding: EdgeInsets.zero, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + tooltip: message, + onPressed: onPressed, + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(5), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + errorIcon, + const SizedBox( + width: 10, + ), + Flexible( + child: Text( + details.getText(context), + style: TextStyle( + color: color, + ), + ), + ), + ], + ), + ElevatedButton( + onPressed: onPressed, + child: Text(message), + ), + ], + ), + ); + } + + /// Gets the details for a given [error]. + @internal + static NeonExceptionDetails getDetails(final Object? error) { + switch (error) { + case String(): + return NeonExceptionDetails( + getText: (final _) => error, + ); + case NeonException(): + return error.details; + case DynamiteApiException(): + if (error.statusCode == 401) { + return NeonExceptionDetails( + getText: (final context) => AppLocalizations.of(context).errorCredentialsForAccountNoLongerMatch, + isUnauthorized: true, + ); + } + if (error.statusCode >= 500 && error.statusCode <= 599) { + return NeonExceptionDetails( + getText: (final context) => AppLocalizations.of(context).errorServerHadAProblemProcessingYourRequest, + ); + } + case SocketException(): + return NeonExceptionDetails( + getText: (final context) => error.address != null + ? AppLocalizations.of(context).errorUnableToReachServerAt(error.address!.host) + : AppLocalizations.of(context).errorUnableToReachServer, + ); + case ClientException(): + return NeonExceptionDetails( + getText: (final context) => error.uri != null + ? AppLocalizations.of(context).errorUnableToReachServerAt(error.uri!.host) + : AppLocalizations.of(context).errorUnableToReachServer, + ); + case HttpException(): + return NeonExceptionDetails( + getText: (final context) => error.uri != null + ? AppLocalizations.of(context).errorUnableToReachServerAt(error.uri!.host) + : AppLocalizations.of(context).errorUnableToReachServer, + ); + case TimeoutException(): + return NeonExceptionDetails( + getText: (final context) => AppLocalizations.of(context).errorConnectionTimedOut, + ); + } + + return NeonExceptionDetails( + getText: (final context) => AppLocalizations.of(context).errorSomethingWentWrongTryAgainLater, + ); + } + + static void _openLoginPage(final BuildContext context) { + unawaited( + LoginCheckServerStatusRoute( + serverUrl: NeonProvider.of(context).activeAccount.value!.serverURL, + ).push(context), + ); + } +} diff --git a/packages/neon/neon/lib/src/widgets/exception.dart b/packages/neon/neon/lib/src/widgets/exception.dart deleted file mode 100644 index ff1cd0aa..00000000 --- a/packages/neon/neon/lib/src/widgets/exception.dart +++ /dev/null @@ -1,207 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:http/http.dart'; -import 'package:meta/meta.dart'; -import 'package:neon/l10n/localizations.dart'; -import 'package:neon/src/blocs/accounts.dart'; -import 'package:neon/src/router.dart'; -import 'package:neon/src/utils/exceptions.dart'; -import 'package:neon/src/utils/provider.dart'; -import 'package:nextcloud/nextcloud.dart'; - -class NeonException extends StatelessWidget { - const NeonException( - this.exception, { - required this.onRetry, - this.onlyIcon = false, - this.iconSize, - this.color, - super.key, - }); - - final dynamic exception; - final VoidCallback onRetry; - final bool onlyIcon; - final double? iconSize; - final Color? color; - - static void showSnackbar(final BuildContext context, final dynamic exception) { - final details = getDetails(context, exception); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(details.text), - action: details.isUnauthorized - ? SnackBarAction( - label: AppLocalizations.of(context).loginAgain, - onPressed: () => _openLoginPage(context), - ) - : null, - ), - ); - } - - @override - Widget build(final BuildContext context) { - if (exception == null) { - return const SizedBox(); - } - - final details = getDetails(context, exception); - final color = this.color ?? Theme.of(context).colorScheme.error; - - final errorIcon = Icon( - Icons.error_outline, - size: iconSize ?? 30, - color: color, - ); - - final message = - details.isUnauthorized ? AppLocalizations.of(context).loginAgain : AppLocalizations.of(context).actionRetry; - - final onPressed = details.isUnauthorized ? () => _openLoginPage(context) : onRetry; - - if (onlyIcon) { - return Semantics( - tooltip: details.text, - child: IconButton( - icon: errorIcon, - padding: EdgeInsets.zero, - visualDensity: const VisualDensity( - horizontal: VisualDensity.minimumDensity, - vertical: VisualDensity.minimumDensity, - ), - tooltip: message, - onPressed: onPressed, - ), - ); - } - - return Padding( - padding: const EdgeInsets.all(5), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - errorIcon, - const SizedBox( - width: 10, - ), - Flexible( - child: Text( - details.text, - style: TextStyle( - color: color, - ), - ), - ), - ], - ), - ElevatedButton( - onPressed: onPressed, - child: Text(message), - ), - ], - ), - ); - } - - @internal - static ExceptionDetails getDetails(final BuildContext context, final dynamic exception) { - if (exception is String) { - return ExceptionDetails( - text: exception, - ); - } - - if (exception is MissingPermissionException) { - return ExceptionDetails( - text: AppLocalizations.of(context).errorMissingPermission(exception.permission.toString().split('.')[1]), - ); - } - - if (exception is UnableToOpenFileException) { - return ExceptionDetails( - text: AppLocalizations.of(context).errorUnableToOpenFile, - ); - } - - if (exception is InvalidQRcodeException) { - return ExceptionDetails( - text: AppLocalizations.of(context).errorInvalidQRcode, - ); - } - - if (exception is DynamiteApiException) { - if (exception.statusCode == 401) { - return ExceptionDetails( - text: AppLocalizations.of(context).errorCredentialsForAccountNoLongerMatch, - isUnauthorized: true, - ); - } - - if (exception.statusCode >= 500 && exception.statusCode <= 599) { - return ExceptionDetails( - text: AppLocalizations.of(context).errorServerHadAProblemProcessingYourRequest, - ); - } - } - - if (exception is SocketException) { - return ExceptionDetails( - text: exception.address != null - ? AppLocalizations.of(context).errorUnableToReachServerAt(exception.address!.host) - : AppLocalizations.of(context).errorUnableToReachServer, - ); - } - - if (exception is ClientException) { - return ExceptionDetails( - text: exception.uri != null - ? AppLocalizations.of(context).errorUnableToReachServerAt(exception.uri!.host) - : AppLocalizations.of(context).errorUnableToReachServer, - ); - } - - if (exception is HttpException) { - return ExceptionDetails( - text: exception.uri != null - ? AppLocalizations.of(context).errorUnableToReachServerAt(exception.uri!.host) - : AppLocalizations.of(context).errorUnableToReachServer, - ); - } - - if (exception is TimeoutException) { - return ExceptionDetails( - text: AppLocalizations.of(context).errorConnectionTimedOut, - ); - } - - return ExceptionDetails( - text: AppLocalizations.of(context).errorSomethingWentWrongTryAgainLater, - ); - } - - static void _openLoginPage(final BuildContext context) { - unawaited( - LoginCheckServerStatusRoute( - serverUrl: NeonProvider.of(context).activeAccount.value!.serverURL, - ).push(context), - ); - } -} - -@internal -class ExceptionDetails { - ExceptionDetails({ - required this.text, - this.isUnauthorized = false, - }); - - final String text; - final bool isUnauthorized; -} diff --git a/packages/neon/neon/lib/src/widgets/list_view.dart b/packages/neon/neon/lib/src/widgets/list_view.dart index c33aae02..28035cde 100644 --- a/packages/neon/neon/lib/src/widgets/list_view.dart +++ b/packages/neon/neon/lib/src/widgets/list_view.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; class NeonListView extends StatelessWidget { @@ -18,7 +18,7 @@ class NeonListView extends StatelessWidget { final Iterable? items; final bool isLoading; - final dynamic error; + final Object? error; final RefreshCallback onRefresh; final Widget Function(BuildContext, T data) builder; final String? scrollKey; @@ -47,7 +47,7 @@ class NeonListView extends StatelessWidget { padding: withFloatingActionButton ? const EdgeInsets.only(bottom: 88) : null, children: [ ...?topScrollingChildren, - NeonException( + NeonError( error, onRetry: onRefresh, ), diff --git a/packages/neon/neon/lib/src/widgets/unified_search_results.dart b/packages/neon/neon/lib/src/widgets/unified_search_results.dart index b3a419fc..1cad5370 100644 --- a/packages/neon/neon/lib/src/widgets/unified_search_results.dart +++ b/packages/neon/neon/lib/src/widgets/unified_search_results.dart @@ -10,7 +10,7 @@ import 'package:neon/src/models/account.dart'; import 'package:neon/src/theme/sizes.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/widgets/cached_image.dart'; -import 'package:neon/src/widgets/exception.dart'; +import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/image_wrapper.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/list_view.dart'; @@ -66,7 +66,7 @@ class NeonUnifiedSearchResults extends StatelessWidget { provider.name, style: Theme.of(context).textTheme.headlineSmall, ), - NeonException( + NeonError( result.error, onRetry: bloc.refresh, ), diff --git a/packages/neon/neon/lib/widgets.dart b/packages/neon/neon/lib/widgets.dart index 09d78aee..34bee9ed 100644 --- a/packages/neon/neon/lib/widgets.dart +++ b/packages/neon/neon/lib/widgets.dart @@ -1,6 +1,6 @@ export 'package:neon/src/widgets/cached_image.dart'; export 'package:neon/src/widgets/dialog.dart'; -export 'package:neon/src/widgets/exception.dart' hide ExceptionDetails; +export 'package:neon/src/widgets/error.dart'; export 'package:neon/src/widgets/image_wrapper.dart'; export 'package:neon/src/widgets/linear_progress_indicator.dart'; export 'package:neon/src/widgets/list_view.dart'; diff --git a/packages/neon/neon_files/lib/blocs/files.dart b/packages/neon/neon_files/lib/blocs/files.dart index c61f730d..b8aae030 100644 --- a/packages/neon/neon_files/lib/blocs/files.dart +++ b/packages/neon/neon_files/lib/blocs/files.dart @@ -95,7 +95,7 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta } final result = await OpenFile.open(file.path, type: mimeType); if (result.type != ResultType.done) { - throw UnableToOpenFileException(); + throw const UnableToOpenFileException(); } }, disableTimeout: true, @@ -194,3 +194,13 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta _uploadQueue.parallel = options.uploadQueueParallelism.value; } } + +@immutable +class UnableToOpenFileException extends NeonException { + const UnableToOpenFileException(); + + @override + NeonExceptionDetails get details => NeonExceptionDetails( + getText: (final context) => AppLocalizations.of(context).errorUnableToOpenFile, + ); +} diff --git a/packages/neon/neon_files/lib/l10n/en.arb b/packages/neon/neon_files/lib/l10n/en.arb index 07661702..8a347043 100644 --- a/packages/neon/neon_files/lib/l10n/en.arb +++ b/packages/neon/neon_files/lib/l10n/en.arb @@ -7,6 +7,7 @@ "actionMove": "Move", "actionCopy": "Copy", "actionSync": "Sync", + "errorUnableToOpenFile": "Unable to open the file", "general": "General", "goToPath": "Go to /{path}", "@goToPath": { diff --git a/packages/neon/neon_files/lib/l10n/localizations.dart b/packages/neon/neon_files/lib/l10n/localizations.dart index f0372a5e..1914ad2c 100644 --- a/packages/neon/neon_files/lib/l10n/localizations.dart +++ b/packages/neon/neon_files/lib/l10n/localizations.dart @@ -131,6 +131,12 @@ abstract class AppLocalizations { /// **'Sync'** String get actionSync; + /// No description provided for @errorUnableToOpenFile. + /// + /// In en, this message translates to: + /// **'Unable to open the file'** + String get errorUnableToOpenFile; + /// No description provided for @general. /// /// In en, this message translates to: diff --git a/packages/neon/neon_files/lib/l10n/localizations_en.dart b/packages/neon/neon_files/lib/l10n/localizations_en.dart index 622b0ae2..adb1bd35 100644 --- a/packages/neon/neon_files/lib/l10n/localizations_en.dart +++ b/packages/neon/neon_files/lib/l10n/localizations_en.dart @@ -25,6 +25,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get actionSync => 'Sync'; + @override + String get errorUnableToOpenFile => 'Unable to open the file'; + @override String get general => 'General'; diff --git a/packages/neon/neon_files/lib/pages/main.dart b/packages/neon/neon_files/lib/pages/main.dart index 1dad7796..ca993fe0 100644 --- a/packages/neon/neon_files/lib/pages/main.dart +++ b/packages/neon/neon_files/lib/pages/main.dart @@ -18,7 +18,7 @@ class _FilesMainPageState extends State { bloc = NeonProvider.of(context); bloc.errors.listen((final error) { - NeonException.showSnackbar(context, error); + NeonError.showSnackbar(context, error); }); } diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index f1f6d140..cc77c376 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -34,7 +34,7 @@ class _FilesBrowserViewState extends State { @override void initState() { widget.bloc.errors.listen((final error) { - NeonException.showSnackbar(context, error); + NeonError.showSnackbar(context, error); }); super.initState(); diff --git a/packages/neon/neon_news/lib/dialogs/add_feed.dart b/packages/neon/neon_news/lib/dialogs/add_feed.dart index 4c640a29..d212fe38 100644 --- a/packages/neon/neon_news/lib/dialogs/add_feed.dart +++ b/packages/neon/neon_news/lib/dialogs/add_feed.dart @@ -73,7 +73,7 @@ class _NewsAddFeedDialogState extends State { ), if (widget.folderID == null) ...[ Center( - child: NeonException( + child: NeonError( folders.error, onRetry: widget.bloc.refresh, ), diff --git a/packages/neon/neon_news/lib/pages/article.dart b/packages/neon/neon_news/lib/pages/article.dart index 43a61870..48e0f4f5 100644 --- a/packages/neon/neon_news/lib/pages/article.dart +++ b/packages/neon/neon_news/lib/pages/article.dart @@ -30,7 +30,7 @@ class _NewsArticlePageState extends State { super.initState(); widget.bloc.errors.listen((final error) { - NeonException.showSnackbar(context, error); + NeonError.showSnackbar(context, error); }); WidgetsBinding.instance.addPostFrameCallback((final _) { diff --git a/packages/neon/neon_news/lib/pages/main.dart b/packages/neon/neon_news/lib/pages/main.dart index e3390279..65e90c6a 100644 --- a/packages/neon/neon_news/lib/pages/main.dart +++ b/packages/neon/neon_news/lib/pages/main.dart @@ -19,7 +19,7 @@ class _NewsMainPageState extends State { bloc = NeonProvider.of(context); bloc.errors.listen((final error) { - NeonException.showSnackbar(context, error); + NeonError.showSnackbar(context, error); }); } diff --git a/packages/neon/neon_news/lib/widgets/articles_view.dart b/packages/neon/neon_news/lib/widgets/articles_view.dart index f14bad91..4a31fcc3 100644 --- a/packages/neon/neon_news/lib/widgets/articles_view.dart +++ b/packages/neon/neon_news/lib/widgets/articles_view.dart @@ -20,7 +20,7 @@ class _NewsArticlesViewState extends State { super.initState(); widget.bloc.errors.listen((final error) { - NeonException.showSnackbar(context, error); + NeonError.showSnackbar(context, error); }); } diff --git a/packages/neon/neon_notes/lib/dialogs/create_note.dart b/packages/neon/neon_notes/lib/dialogs/create_note.dart index fd3d8598..fc99c4c6 100644 --- a/packages/neon/neon_notes/lib/dialogs/create_note.dart +++ b/packages/neon/neon_notes/lib/dialogs/create_note.dart @@ -55,7 +55,7 @@ class _NotesCreateNoteDialogState extends State { ), if (widget.category == null) ...[ Center( - child: NeonException( + child: NeonError( notes.error, onRetry: widget.bloc.refresh, ), diff --git a/packages/neon/neon_notes/lib/dialogs/select_category.dart b/packages/neon/neon_notes/lib/dialogs/select_category.dart index 2cec920e..e8d8c956 100644 --- a/packages/neon/neon_notes/lib/dialogs/select_category.dart +++ b/packages/neon/neon_notes/lib/dialogs/select_category.dart @@ -37,7 +37,7 @@ class _NotesSelectCategoryDialogState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ Center( - child: NeonException( + child: NeonError( notes.error, onRetry: widget.bloc.refresh, ), diff --git a/packages/neon/neon_notes/lib/utils/exception_handler.dart b/packages/neon/neon_notes/lib/utils/exception_handler.dart index 82e982c7..d658f1c3 100644 --- a/packages/neon/neon_notes/lib/utils/exception_handler.dart +++ b/packages/neon/neon_notes/lib/utils/exception_handler.dart @@ -2,8 +2,8 @@ part of '../neon_notes.dart'; void handleNotesException(final BuildContext context, final Object error) { if (error is DynamiteApiException && error.statusCode == 412) { - NeonException.showSnackbar(context, AppLocalizations.of(context).errorChangedOnServer); + NeonError.showSnackbar(context, AppLocalizations.of(context).errorChangedOnServer); } else { - NeonException.showSnackbar(context, error); + NeonError.showSnackbar(context, error); } } diff --git a/packages/neon/neon_notifications/lib/pages/main.dart b/packages/neon/neon_notifications/lib/pages/main.dart index 84add132..34d2b4cd 100644 --- a/packages/neon/neon_notifications/lib/pages/main.dart +++ b/packages/neon/neon_notifications/lib/pages/main.dart @@ -19,7 +19,7 @@ class _NotificationsMainPageState extends State { bloc = NeonProvider.of(context) as NotificationsBloc; bloc.errors.listen((final error) { - NeonException.showSnackbar(context, error); + NeonError.showSnackbar(context, error); }); }