Browse Source

Merge pull request #852 from nextcloud/refactor/abstract-exceptions

Refactor/abstract exceptions
pull/862/head
Kate 1 year ago committed by GitHub
parent
commit
fe019b56c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/neon/neon/lib/l10n/en.arb
  2. 6
      packages/neon/neon/lib/l10n/localizations.dart
  3. 3
      packages/neon/neon/lib/l10n/localizations_en.dart
  4. 4
      packages/neon/neon/lib/src/pages/account_settings.dart
  5. 4
      packages/neon/neon/lib/src/pages/home.dart
  6. 8
      packages/neon/neon/lib/src/pages/login_check_account.dart
  7. 4
      packages/neon/neon/lib/src/pages/login_check_server_status.dart
  8. 4
      packages/neon/neon/lib/src/pages/login_flow.dart
  9. 19
      packages/neon/neon/lib/src/pages/login_qr_code.dart
  10. 8
      packages/neon/neon/lib/src/pages/settings.dart
  11. 2
      packages/neon/neon/lib/src/platform/android.dart
  12. 47
      packages/neon/neon/lib/src/utils/exceptions.dart
  13. 4
      packages/neon/neon/lib/src/widgets/account_tile.dart
  14. 4
      packages/neon/neon/lib/src/widgets/app_bar.dart
  15. 6
      packages/neon/neon/lib/src/widgets/cached_image.dart
  16. 4
      packages/neon/neon/lib/src/widgets/drawer.dart
  17. 191
      packages/neon/neon/lib/src/widgets/error.dart
  18. 207
      packages/neon/neon/lib/src/widgets/exception.dart
  19. 6
      packages/neon/neon/lib/src/widgets/list_view.dart
  20. 4
      packages/neon/neon/lib/src/widgets/unified_search_results.dart
  21. 2
      packages/neon/neon/lib/widgets.dart
  22. 12
      packages/neon/neon_files/lib/blocs/files.dart
  23. 1
      packages/neon/neon_files/lib/l10n/en.arb
  24. 6
      packages/neon/neon_files/lib/l10n/localizations.dart
  25. 3
      packages/neon/neon_files/lib/l10n/localizations_en.dart
  26. 2
      packages/neon/neon_files/lib/pages/main.dart
  27. 2
      packages/neon/neon_files/lib/widgets/browser_view.dart
  28. 2
      packages/neon/neon_news/lib/dialogs/add_feed.dart
  29. 2
      packages/neon/neon_news/lib/pages/article.dart
  30. 2
      packages/neon/neon_news/lib/pages/main.dart
  31. 2
      packages/neon/neon_news/lib/widgets/articles_view.dart
  32. 2
      packages/neon/neon_notes/lib/dialogs/create_note.dart
  33. 2
      packages/neon/neon_notes/lib/dialogs/select_category.dart
  34. 4
      packages/neon/neon_notes/lib/utils/exception_handler.dart
  35. 2
      packages/neon/neon_notifications/lib/pages/main.dart

1
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": "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" : { "@errorUnsupportedAppVersions" : {
"placeholders": { "placeholders": {

6
packages/neon/neon/lib/l10n/localizations.dart

@ -239,12 +239,6 @@ abstract class AppLocalizations {
/// **'Permission for {name} is missing'** /// **'Permission for {name} is missing'**
String errorMissingPermission(String name); 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. /// No description provided for @errorUnsupportedAppVersions.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

3
packages/neon/neon/lib/l10n/localizations_en.dart

@ -106,9 +106,6 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Permission for $name is missing'; return 'Permission for $name is missing';
} }
@override
String get errorUnableToOpenFile => 'Unable to open the file';
@override @override
String errorUnsupportedAppVersions(String names) { 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.'; 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.';

4
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/settings/widgets/settings_list.dart';
import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/utils/confirmation_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:neon/src/widgets/linear_progress_indicator.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
@ -104,7 +104,7 @@ class AccountSettingsPage extends StatelessWidget {
), ),
), ),
], ],
NeonException( NeonError(
userDetails.error, userDetails.error,
onRetry: userDetailsBloc.refresh, onRetry: userDetailsBloc.refresh,
), ),

4
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/utils/provider.dart';
import 'package:neon/src/widgets/app_bar.dart'; import 'package:neon/src/widgets/app_bar.dart';
import 'package:neon/src/widgets/drawer.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:neon/src/widgets/unified_search_results.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -91,7 +91,7 @@ class _HomePageState extends State<HomePage> {
debugPrint(e.toString()); debugPrint(e.toString());
debugPrint(s.toString()); debugPrint(s.toString());
if (mounted) { if (mounted) {
NeonException.showSnackbar(context, e); NeonError.showSnackbar(context, e);
} }
} }
} }

8
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/theme/dialog.dart';
import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/provider.dart';
import 'package:neon/src/widgets/account_tile.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'; import 'package:neon/src/widgets/validation_tile.dart';
@internal @internal
@ -68,11 +68,11 @@ class _LoginCheckAccountPageState extends State<LoginCheckAccountPage> {
if (state.hasError) ...[ if (state.hasError) ...[
Builder( Builder(
builder: (final context) { builder: (final context) {
final details = NeonException.getDetails(context, state.error); final details = NeonError.getDetails(state.error);
return NeonValidationTile( return NeonValidationTile(
title: details.isUnauthorized title: details.isUnauthorized
? AppLocalizations.of(context).errorCredentialsForAccountNoLongerMatch ? AppLocalizations.of(context).errorCredentialsForAccountNoLongerMatch
: details.text, : details.getText(context),
state: ValidationState.failure, state: ValidationState.failure,
); );
}, },
@ -91,7 +91,7 @@ class _LoginCheckAccountPageState extends State<LoginCheckAccountPage> {
const HomeRoute().go(context); const HomeRoute().go(context);
} }
: () { : () {
if (state.hasError && NeonException.getDetails(context, state.error).isUnauthorized) { if (state.hasError && NeonError.getDetails(state.error).isUnauthorized) {
Navigator.pop(context); Navigator.pop(context);
return; return;
} }

4
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/blocs/login_check_server_status.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/theme/dialog.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:neon/src/widgets/validation_tile.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
@ -67,7 +67,7 @@ class _LoginCheckServerStatusPageState extends State<LoginCheckServerStatusPage>
children: [ children: [
if (state.hasError) ...[ if (state.hasError) ...[
NeonValidationTile( NeonValidationTile(
title: NeonException.getDetails(context, state.error).text, title: NeonError.getDetails(state.error).getText(context),
state: ValidationState.failure, state: ValidationState.failure,
), ),
], ],

4
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/bloc/result_builder.dart';
import 'package:neon/src/blocs/login_flow.dart'; import 'package:neon/src/blocs/login_flow.dart';
import 'package:neon/src/router.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:neon/src/widgets/linear_progress_indicator.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -68,7 +68,7 @@ class _LoginFlowPageState extends State<LoginFlowPage> {
NeonLinearProgressIndicator( NeonLinearProgressIndicator(
visible: init.isLoading, visible: init.isLoading,
), ),
NeonException( NeonError(
init.error, init.error,
onRetry: bloc.refresh, onRetry: bloc.refresh,
), ),

19
packages/neon/neon/lib/src/pages/login_qr_code.dart

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_zxing/flutter_zxing.dart'; import 'package:flutter_zxing/flutter_zxing.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/utils/exceptions.dart'; import 'package:neon/src/utils/exceptions.dart';
import 'package:neon/src/widgets/exception.dart'; import 'package:neon/src/widgets/error.dart';
@internal @internal
class LoginQRcodePage extends StatefulWidget { class LoginQRcodePage extends StatefulWidget {
@ -35,11 +36,11 @@ class _LoginQRcodePageState extends State<LoginQRcodePage> {
try { try {
url = code.text; url = code.text;
if (url == null) { if (url == null) {
throw InvalidQRcodeException(); throw const InvalidQRcodeException();
} }
final match = LoginQRcode.tryParse(url); final match = LoginQRcode.tryParse(url);
if (match == null) { if (match == null) {
throw InvalidQRcodeException(); throw const InvalidQRcodeException();
} }
LoginCheckServerStatusRoute.withCredentials( LoginCheckServerStatusRoute.withCredentials(
@ -53,10 +54,20 @@ class _LoginQRcodePageState extends State<LoginQRcodePage> {
debugPrint(s.toString()); debugPrint(s.toString());
_lastErrorURL = url; _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,
);
}

8
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/global_options.dart';
import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/provider.dart';
import 'package:neon/src/utils/save_file.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:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -287,7 +287,7 @@ class _SettingsPageState extends State<SettingsPage> {
debugPrint(e.toString()); debugPrint(e.toString());
debugPrint(s.toString()); debugPrint(s.toString());
if (mounted) { if (mounted) {
NeonException.showSnackbar(context, e); NeonError.showSnackbar(context, e);
} }
} }
}, },
@ -312,7 +312,7 @@ class _SettingsPageState extends State<SettingsPage> {
if (!result.files.single.path!.endsWith('.json')) { if (!result.files.single.path!.endsWith('.json')) {
if (mounted) { if (mounted) {
NeonException.showSnackbar( NeonError.showSnackbar(
context, context,
AppLocalizations.of(context).settingsImportWrongFileExtension, AppLocalizations.of(context).settingsImportWrongFileExtension,
); );
@ -325,7 +325,7 @@ class _SettingsPageState extends State<SettingsPage> {
debugPrint(e.toString()); debugPrint(e.toString());
debugPrint(s.toString()); debugPrint(s.toString());
if (mounted) { if (mounted) {
NeonException.showSnackbar(context, e); NeonError.showSnackbar(context, e);
} }
} }
}, },

2
packages/neon/neon/lib/src/platform/android.dart

@ -31,7 +31,7 @@ class AndroidNeonPlatform implements NeonPlatform {
@override @override
Future<String> get userAccessibleAppDataPath async { Future<String> get userAccessibleAppDataPath async {
if (!await Permission.storage.request().isGranted) { if (!await Permission.storage.request().isGranted) {
throw MissingPermissionException(Permission.storage); throw const MissingPermissionException(Permission.storage);
} }
return p.join((await getExternalStorageDirectory())!.path); return p.join((await getExternalStorageDirectory())!.path);

47
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'; import 'package:permission_handler/permission_handler.dart';
class MissingPermissionException implements Exception { /// Details of a [NeonException].
MissingPermissionException(this.permission); 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]),
);
}

4
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/blocs/accounts.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/utils/provider.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/linear_progress_indicator.dart';
import 'package:neon/src/widgets/user_avatar.dart'; import 'package:neon/src/widgets/user_avatar.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
@ -79,7 +79,7 @@ class NeonAccountTile extends StatelessWidget {
const SizedBox( const SizedBox(
width: 5, width: 5,
), ),
NeonException( NeonError(
userDetails.error, userDetails.error,
onlyIcon: true, onlyIcon: true,
iconSize: 24, iconSize: 24,

4
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/utils/provider.dart';
import 'package:neon/src/widgets/account_switcher_button.dart'; import 'package:neon/src/widgets/account_switcher_button.dart';
import 'package:neon/src/widgets/app_implementation_icon.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:neon/src/widgets/linear_progress_indicator.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
@ -89,7 +89,7 @@ class _NeonAppBarState extends State<NeonAppBar> {
const SizedBox( const SizedBox(
width: 8, width: 8,
), ),
NeonException( NeonError(
appImplementations.error, appImplementations.error,
onRetry: appsBloc.refresh, onRetry: appsBloc.refresh,
onlyIcon: true, onlyIcon: true,

6
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/blocs/accounts.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/utils/provider.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/linear_progress_indicator.dart';
typedef CacheReviver = FutureOr<Uint8List?> Function(CacheManager cacheManager); typedef CacheReviver = FutureOr<Uint8List?> Function(CacheManager cacheManager);
@ -180,9 +180,9 @@ class _NeonCachedImageState extends State<NeonCachedImage> {
), ),
); );
Widget _buildError(final dynamic error) => Widget _buildError(final Object? error) =>
widget.errorBuilder?.call(context, error) ?? widget.errorBuilder?.call(context, error) ??
NeonException( NeonError(
error, error,
onRetry: () { onRetry: () {
setState(() {}); setState(() {});

4
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/utils/provider.dart';
import 'package:neon/src/widgets/cached_image.dart'; import 'package:neon/src/widgets/cached_image.dart';
import 'package:neon/src/widgets/drawer_destination.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:neon/src/widgets/linear_progress_indicator.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
@ -131,7 +131,7 @@ class NeonDrawerHeader extends StatelessWidget {
} }
if (capabilities.hasError) { if (capabilities.hasError) {
return NeonException( return NeonError(
capabilities.error, capabilities.error,
onRetry: capabilitiesBloc.refresh, onRetry: capabilitiesBloc.refresh,
); );

191
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<AccountsBloc>(context).activeAccount.value!.serverURL,
).push(context),
);
}
}

207
packages/neon/neon/lib/src/widgets/exception.dart

@ -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<AccountsBloc>(context).activeAccount.value!.serverURL,
).push(context),
);
}
}
@internal
class ExceptionDetails {
ExceptionDetails({
required this.text,
this.isUnauthorized = false,
});
final String text;
final bool isUnauthorized;
}

6
packages/neon/neon/lib/src/widgets/list_view.dart

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; 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'; import 'package:neon/src/widgets/linear_progress_indicator.dart';
class NeonListView<T> extends StatelessWidget { class NeonListView<T> extends StatelessWidget {
@ -18,7 +18,7 @@ class NeonListView<T> extends StatelessWidget {
final Iterable<T>? items; final Iterable<T>? items;
final bool isLoading; final bool isLoading;
final dynamic error; final Object? error;
final RefreshCallback onRefresh; final RefreshCallback onRefresh;
final Widget Function(BuildContext, T data) builder; final Widget Function(BuildContext, T data) builder;
final String? scrollKey; final String? scrollKey;
@ -47,7 +47,7 @@ class NeonListView<T> extends StatelessWidget {
padding: withFloatingActionButton ? const EdgeInsets.only(bottom: 88) : null, padding: withFloatingActionButton ? const EdgeInsets.only(bottom: 88) : null,
children: [ children: [
...?topScrollingChildren, ...?topScrollingChildren,
NeonException( NeonError(
error, error,
onRetry: onRefresh, onRetry: onRefresh,
), ),

4
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/theme/sizes.dart';
import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/provider.dart';
import 'package:neon/src/widgets/cached_image.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/image_wrapper.dart';
import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart';
import 'package:neon/src/widgets/list_view.dart'; import 'package:neon/src/widgets/list_view.dart';
@ -66,7 +66,7 @@ class NeonUnifiedSearchResults extends StatelessWidget {
provider.name, provider.name,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
NeonException( NeonError(
result.error, result.error,
onRetry: bloc.refresh, onRetry: bloc.refresh,
), ),

2
packages/neon/neon/lib/widgets.dart

@ -1,6 +1,6 @@
export 'package:neon/src/widgets/cached_image.dart'; export 'package:neon/src/widgets/cached_image.dart';
export 'package:neon/src/widgets/dialog.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/image_wrapper.dart';
export 'package:neon/src/widgets/linear_progress_indicator.dart'; export 'package:neon/src/widgets/linear_progress_indicator.dart';
export 'package:neon/src/widgets/list_view.dart'; export 'package:neon/src/widgets/list_view.dart';

12
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); final result = await OpenFile.open(file.path, type: mimeType);
if (result.type != ResultType.done) { if (result.type != ResultType.done) {
throw UnableToOpenFileException(); throw const UnableToOpenFileException();
} }
}, },
disableTimeout: true, disableTimeout: true,
@ -194,3 +194,13 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
_uploadQueue.parallel = options.uploadQueueParallelism.value; _uploadQueue.parallel = options.uploadQueueParallelism.value;
} }
} }
@immutable
class UnableToOpenFileException extends NeonException {
const UnableToOpenFileException();
@override
NeonExceptionDetails get details => NeonExceptionDetails(
getText: (final context) => AppLocalizations.of(context).errorUnableToOpenFile,
);
}

1
packages/neon/neon_files/lib/l10n/en.arb

@ -7,6 +7,7 @@
"actionMove": "Move", "actionMove": "Move",
"actionCopy": "Copy", "actionCopy": "Copy",
"actionSync": "Sync", "actionSync": "Sync",
"errorUnableToOpenFile": "Unable to open the file",
"general": "General", "general": "General",
"goToPath": "Go to /{path}", "goToPath": "Go to /{path}",
"@goToPath": { "@goToPath": {

6
packages/neon/neon_files/lib/l10n/localizations.dart

@ -131,6 +131,12 @@ abstract class AppLocalizations {
/// **'Sync'** /// **'Sync'**
String get actionSync; 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. /// No description provided for @general.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

3
packages/neon/neon_files/lib/l10n/localizations_en.dart

@ -25,6 +25,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get actionSync => 'Sync'; String get actionSync => 'Sync';
@override
String get errorUnableToOpenFile => 'Unable to open the file';
@override @override
String get general => 'General'; String get general => 'General';

2
packages/neon/neon_files/lib/pages/main.dart

@ -18,7 +18,7 @@ class _FilesMainPageState extends State<FilesMainPage> {
bloc = NeonProvider.of<FilesBloc>(context); bloc = NeonProvider.of<FilesBloc>(context);
bloc.errors.listen((final error) { bloc.errors.listen((final error) {
NeonException.showSnackbar(context, error); NeonError.showSnackbar(context, error);
}); });
} }

2
packages/neon/neon_files/lib/widgets/browser_view.dart

@ -34,7 +34,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
@override @override
void initState() { void initState() {
widget.bloc.errors.listen((final error) { widget.bloc.errors.listen((final error) {
NeonException.showSnackbar(context, error); NeonError.showSnackbar(context, error);
}); });
super.initState(); super.initState();

2
packages/neon/neon_news/lib/dialogs/add_feed.dart

@ -73,7 +73,7 @@ class _NewsAddFeedDialogState extends State<NewsAddFeedDialog> {
), ),
if (widget.folderID == null) ...[ if (widget.folderID == null) ...[
Center( Center(
child: NeonException( child: NeonError(
folders.error, folders.error,
onRetry: widget.bloc.refresh, onRetry: widget.bloc.refresh,
), ),

2
packages/neon/neon_news/lib/pages/article.dart

@ -30,7 +30,7 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
super.initState(); super.initState();
widget.bloc.errors.listen((final error) { widget.bloc.errors.listen((final error) {
NeonException.showSnackbar(context, error); NeonError.showSnackbar(context, error);
}); });
WidgetsBinding.instance.addPostFrameCallback((final _) { WidgetsBinding.instance.addPostFrameCallback((final _) {

2
packages/neon/neon_news/lib/pages/main.dart

@ -19,7 +19,7 @@ class _NewsMainPageState extends State<NewsMainPage> {
bloc = NeonProvider.of<NewsBloc>(context); bloc = NeonProvider.of<NewsBloc>(context);
bloc.errors.listen((final error) { bloc.errors.listen((final error) {
NeonException.showSnackbar(context, error); NeonError.showSnackbar(context, error);
}); });
} }

2
packages/neon/neon_news/lib/widgets/articles_view.dart

@ -20,7 +20,7 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
super.initState(); super.initState();
widget.bloc.errors.listen((final error) { widget.bloc.errors.listen((final error) {
NeonException.showSnackbar(context, error); NeonError.showSnackbar(context, error);
}); });
} }

2
packages/neon/neon_notes/lib/dialogs/create_note.dart

@ -55,7 +55,7 @@ class _NotesCreateNoteDialogState extends State<NotesCreateNoteDialog> {
), ),
if (widget.category == null) ...[ if (widget.category == null) ...[
Center( Center(
child: NeonException( child: NeonError(
notes.error, notes.error,
onRetry: widget.bloc.refresh, onRetry: widget.bloc.refresh,
), ),

2
packages/neon/neon_notes/lib/dialogs/select_category.dart

@ -37,7 +37,7 @@ class _NotesSelectCategoryDialogState extends State<NotesSelectCategoryDialog> {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Center( Center(
child: NeonException( child: NeonError(
notes.error, notes.error,
onRetry: widget.bloc.refresh, onRetry: widget.bloc.refresh,
), ),

4
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) { void handleNotesException(final BuildContext context, final Object error) {
if (error is DynamiteApiException && error.statusCode == 412) { if (error is DynamiteApiException && error.statusCode == 412) {
NeonException.showSnackbar(context, AppLocalizations.of(context).errorChangedOnServer); NeonError.showSnackbar(context, AppLocalizations.of(context).errorChangedOnServer);
} else { } else {
NeonException.showSnackbar(context, error); NeonError.showSnackbar(context, error);
} }
} }

2
packages/neon/neon_notifications/lib/pages/main.dart

@ -19,7 +19,7 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
bloc = NeonProvider.of<NotificationsBlocInterface>(context) as NotificationsBloc; bloc = NeonProvider.of<NotificationsBlocInterface>(context) as NotificationsBloc;
bloc.errors.listen((final error) { bloc.errors.listen((final error) {
NeonException.showSnackbar(context, error); NeonError.showSnackbar(context, error);
}); });
} }

Loading…
Cancel
Save