diff --git a/lib/core/bloc/paperless_server_information_cubit.dart b/lib/core/bloc/paperless_server_information_cubit.dart new file mode 100644 index 0000000..4764c43 --- /dev/null +++ b/lib/core/bloc/paperless_server_information_cubit.dart @@ -0,0 +1,17 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/core/model/paperless_server_information.dart'; +import 'package:paperless_mobile/core/service/paperless_server_information_service.dart'; + +@singleton +class PaperlessServerInformationCubit + extends Cubit { + final PaperlessServerInformationService service; + + PaperlessServerInformationCubit(this.service) + : super(PaperlessServerInformation()); + + Future updateStatus() async { + emit(await service.getInformation()); + } +} diff --git a/lib/core/model/github_error_report.model.dart b/lib/core/model/github_error_report.model.dart new file mode 100644 index 0000000..3019acd --- /dev/null +++ b/lib/core/model/github_error_report.model.dart @@ -0,0 +1,9 @@ +class GithubErrorReport { + final String? shortDescription; + final String? longDescription; + + GithubErrorReport({ + this.shortDescription, + this.longDescription, + }); +} diff --git a/lib/core/model/paperless_server_information.dart b/lib/core/model/paperless_server_information.dart new file mode 100644 index 0000000..aabece9 --- /dev/null +++ b/lib/core/model/paperless_server_information.dart @@ -0,0 +1,15 @@ +class PaperlessServerInformation { + static const String versionHeader = 'x-version'; + static const String apiVersionHeader = 'x-api-version'; + static const String hostHeader = 'x-served-by'; + final String? version; + final int? apiVersion; + final String? username; + final String? host; + PaperlessServerInformation({ + this.host, + this.username, + this.version = 'unknown', + this.apiVersion = 1, + }); +} diff --git a/lib/core/service/github_issue_service.dart b/lib/core/service/github_issue_service.dart new file mode 100644 index 0000000..aec0597 --- /dev/null +++ b/lib/core/service/github_issue_service.dart @@ -0,0 +1,61 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_mobile/core/model/github_error_report.model.dart'; +import 'package:paperless_mobile/core/widgets/error_report_page.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:paperless_mobile/extensions/dart_extensions.dart'; + +class GithubIssueService { + static void openCreateGithubIssue({ + String? title, + String? body, + List? labels, + String? milestone, + List? assignees, + String? project, + }) { + final Uri uri = Uri( + scheme: "https", + host: "github.com", + path: "astubenbord/paperless-mobile/issues/new", + queryParameters: {} + ..tryPutIfAbsent('title', () => title) + //..tryPutIfAbsent('body', () => body) //TODO: Figure out how to pass long body via url + ..tryPutIfAbsent('labels', () => labels?.join(',')) + ..tryPutIfAbsent('milestone', () => milestone) + ..tryPutIfAbsent('assignees', () => assignees?.join(',')) + ..tryPutIfAbsent('project', () => project), + ); + log("[GitHubIssueService] Creating GitHub issue: " + uri.toString()); + launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + } + + static void createIssueFromError( + BuildContext context, { + StackTrace? stackTrace, + }) async { + final errorDescription = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ErrorReportPage( + stackTrace: stackTrace, + ), + ), + ); + if (errorDescription == null) { + return; + } + + return openCreateGithubIssue( + title: errorDescription.shortDescription, + body: errorDescription.longDescription ?? '', + labels: ['error report'], + ); + } +} diff --git a/lib/core/service/paperless_server_information_service.dart b/lib/core/service/paperless_server_information_service.dart new file mode 100644 index 0000000..a0fc4cb --- /dev/null +++ b/lib/core/service/paperless_server_information_service.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/core/model/paperless_server_information.dart'; +import 'package:paperless_mobile/core/store/local_vault.dart'; + +@injectable +class PaperlessServerInformationService { + final BaseClient client; + final LocalVault localStore; + + PaperlessServerInformationService( + @Named("timeoutClient") this.client, + this.localStore, + ); + + Future getInformation() async { + final response = await client.get(Uri.parse("/api/ui_settings/")); + final version = + response.headers[PaperlessServerInformation.versionHeader] ?? 'unknown'; + final apiVersion = int.tryParse( + response.headers[PaperlessServerInformation.apiVersionHeader] ?? '1'); + final String username = + jsonDecode(utf8.decode(response.bodyBytes))['username']; + final String? host = + response.headers[PaperlessServerInformation.hostHeader]; + return PaperlessServerInformation( + username: username, + version: version, + apiVersion: apiVersion, + host: host, + ); + } +} diff --git a/lib/core/service/status.service.dart b/lib/core/service/status.service.dart index d96274c..66d34db 100644 --- a/lib/core/service/status.service.dart +++ b/lib/core/service/status.service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:paperless_mobile/core/bloc/document_status_cubit.dart'; import 'package:paperless_mobile/core/model/document_processing_status.dart'; import 'package:paperless_mobile/di_initializer.dart'; @@ -9,7 +10,6 @@ import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/paged_search_result.dart'; import 'package:paperless_mobile/features/login/model/authentication_information.dart'; import 'package:paperless_mobile/util.dart'; -import 'package:http_interceptor/http_interceptor.dart'; import 'package:injectable/injectable.dart'; import 'package:web_socket_channel/io.dart'; diff --git a/lib/core/widgets/error_report_page.dart b/lib/core/widgets/error_report_page.dart new file mode 100644 index 0000000..6098e0a --- /dev/null +++ b/lib/core/widgets/error_report_page.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/src/widgets/container.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:paperless_mobile/core/model/github_error_report.model.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; + +class ErrorReportPage extends StatefulWidget { + final StackTrace? stackTrace; + const ErrorReportPage({super.key, this.stackTrace}); + + @override + State createState() => _ErrorReportPageState(); +} + +class _ErrorReportPageState extends State { + final GlobalKey _formKey = GlobalKey(); + + static const String shortDescriptionKey = "shortDescription"; + static const String longDescriptionKey = "longDescription"; + + bool _stackTraceCopied = false; + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + title: Text("Report error"), + actions: [ + TextButton( + onPressed: _onSubmit, + child: Text("Submit"), + ), + ], + ), + body: FormBuilder( + key: _formKey, + child: ListView( + children: [ + Text( + """Oops, an error has occurred! +In order to improve the app and prevent messages like these, it is greatly appreciated if you report this error with a description of what happened and the actions leading up to this window. +Please fill the fields below and create a new issue in GitHub. Thanks! +Note: If you have the GitHub Android app installed, the descriptions will not be taken into account! Skip these here and fill them in the GitHub issues form after submitting this report.""", + style: Theme.of(context).textTheme.bodyMedium, + ).padded(), + Text( + "Description", + style: Theme.of(context).textTheme.subtitle1, + ).padded(), + FormBuilderTextField( + name: shortDescriptionKey, + decoration: const InputDecoration( + label: Text("Short Description"), + hintText: + "Please provide a brief description of what went wrong."), + ).padded(), + FormBuilderTextField( + name: shortDescriptionKey, + maxLines: null, + keyboardType: TextInputType.multiline, + decoration: const InputDecoration( + label: Text("Detailled Description"), + hintText: + "Please describe the exact actions taken that caused this error. Provide as much details as possible.", + ), + ).padded(), + if (widget.stackTrace != null) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Stack Trace", + style: Theme.of(context).textTheme.subtitle1, + ).padded( + const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0)), + TextButton.icon( + label: const Text("Copy"), + icon: const Icon(Icons.copy), + onPressed: _copyStackTrace, + ), + ], + ), + Text( + "Since stack traces cannot be attached to the GitHub issue url, please copy the content of the stackTrace and paste it in the issue description. This will greatly increase the chance of quickly resolving the issue!", + style: Theme.of(context).textTheme.caption, + ).padded(), + Text( + widget.stackTrace.toString(), + style: Theme.of(context).textTheme.overline, + ).padded(), + ] + ], + ), + ), + ); + } + + void _copyStackTrace() { + Clipboard.setData( + ClipboardData(text: '```${widget.stackTrace.toString()}```'), + ).then( + (_) { + setState(() => _stackTraceCopied = true); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + "Stack trace copied to clipboard.", + ), + duration: Duration(seconds: 2), + ), + ); + }, + ); + } + + void _onSubmit() async { + if (_formKey.currentState?.saveAndValidate() ?? false) { + final fk = _formKey.currentState!.value; + if (!_stackTraceCopied) { + final continueSubmission = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Continue without stack trace?"), + content: const Text( + "It seems you have not yet copied the stack trace. The stack trace provides valuable insights into where an error came from and how it could be fixed. Are you sure you want to continue without providing the stack trace?", + ), + actionsAlignment: MainAxisAlignment.end, + actions: [ + TextButton( + child: const Text("Yes, continue"), + onPressed: () => Navigator.pop(context, true), + ), + TextButton( + child: const Text("No, copy stack trace"), + onPressed: () { + _copyStackTrace(); + Navigator.pop(context, true); + }, + ), + TextButton( + child: const Text("Cancel"), + onPressed: () => Navigator.pop(context, false), + ), + ], + ), + ) ?? + false; + if (!continueSubmission) { + return; + } + } + Navigator.pop( + context, + GithubErrorReport( + shortDescription: fk[shortDescriptionKey], + longDescription: fk[longDescriptionKey], + ), + ); + } + } +} diff --git a/lib/features/documents/view/pages/document_details_page.dart b/lib/features/documents/view/pages/document_details_page.dart index 9fa0370..2dc6cc4 100644 --- a/lib/features/documents/view/pages/document_details_page.dart +++ b/lib/features/documents/view/pages/document_details_page.dart @@ -234,8 +234,8 @@ class _DocumentDetailsPageState extends State { Future _assignAsn(DocumentModel document) async { try { await BlocProvider.of(context).assignAsn(document); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } @@ -409,8 +409,8 @@ class _DocumentDetailsPageState extends State { try { await BlocProvider.of(context).removeDocument(document); showSnackBar(context, S.of(context).documentDeleteSuccessMessage); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } finally { Navigator.pop(context); } diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 5ad6cc6..c3a11b3 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -80,8 +80,8 @@ class _DocumentEditPageState extends State { try { await getIt().updateDocument(updatedDocument); showSnackBar(context, S.of(context).documentUpdateErrorMessage); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } finally { Navigator.pop(context); } diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 426a3af..145f06d 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; +import 'package:paperless_mobile/core/service/github_issue_service.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; @@ -53,8 +54,8 @@ class _DocumentsPageState extends State { Future _initDocuments() async { try { BlocProvider.of(context).loadDocuments(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } @@ -73,9 +74,20 @@ class _DocumentsPageState extends State { } try { await documentsCubit.loadMore(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } + Future.delayed(const Duration(seconds: 1), () { + try { + throw ErrorMessage(ErrorCode.tagLoadFailed); + } on ErrorMessage catch (error, stackTrace) { + showError( + context, + error, + stackTrace, + ); + } + }); } void _onSelected(DocumentModel model) { @@ -87,8 +99,8 @@ class _DocumentsPageState extends State { await BlocProvider.of(context).updateCurrentFilter( (filter) => filter.copyWith(page: 1), ); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 31e8ad8..0bb0006 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -536,8 +536,8 @@ class _DocumentFilterPanelState extends State { BlocProvider.of(context).resetSelection(); FocusScope.of(context).unfocus(); widget.panelController.close(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart index f38dfd8..ce352aa 100644 --- a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart @@ -87,8 +87,8 @@ class _DocumentsPageAppBarState extends State { context, S.of(context).documentsPageBulkDeleteSuccessfulText, ); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart b/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart index d31f8cc..723373b 100644 --- a/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart +++ b/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart @@ -85,8 +85,8 @@ class SavedViewSelectionWidget extends StatelessWidget { if (newView != null) { try { await BlocProvider.of(context).add(newView); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } @@ -102,8 +102,8 @@ class SavedViewSelectionWidget extends StatelessWidget { BlocProvider.of(context).updateFilter(); BlocProvider.of(context).selectView(null); } - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } @@ -117,8 +117,8 @@ class SavedViewSelectionWidget extends StatelessWidget { if (delete) { try { BlocProvider.of(context).remove(view); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index 2c40e90..b6467d8 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -50,8 +50,8 @@ class _SortDocumentsButtonState extends State { sortOrder: state.filter.sortOrder.toggle(), ), ); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } finally { setState(() => _isLoading = false); } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 495e5f5..90cbc91 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/di_initializer.dart'; @@ -32,7 +32,7 @@ class _HomePageState extends State { @override void initState() { super.initState(); - initializeLabelData(context); + _initializeData(context); } @override @@ -41,7 +41,7 @@ class _HomePageState extends State { //Only re-initialize data if the connectivity changed from not connected to connected listenWhen: (previous, current) => current == ConnectivityState.connected, listener: (context, state) { - initializeLabelData(context); + _initializeData(context); }, builder: (context, connectivityState) { return Scaffold( @@ -73,15 +73,17 @@ class _HomePageState extends State { ); } - initializeLabelData(BuildContext context) { + _initializeData(BuildContext context) async { try { + await BlocProvider.of(context) + .updateStatus(); BlocProvider.of(context).initialize(); BlocProvider.of(context).initialize(); BlocProvider.of(context).initialize(); BlocProvider.of(context).initialize(); BlocProvider.of(context).initialize(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/home/view/widget/bottom_navigation_bar.dart b/lib/features/home/view/widget/bottom_navigation_bar.dart index bd347f0..d97ff22 100644 --- a/lib/features/home/view/widget/bottom_navigation_bar.dart +++ b/lib/features/home/view/widget/bottom_navigation_bar.dart @@ -19,15 +19,29 @@ class BottomNavBar extends StatelessWidget { selectedIndex: selectedIndex, destinations: [ NavigationDestination( - icon: const Icon(Icons.description), + icon: const Icon(Icons.description_outlined), + selectedIcon: Icon( + Icons.description, + color: Theme.of(context).colorScheme.primary, + ), label: S.of(context).bottomNavDocumentsPageLabel, ), NavigationDestination( icon: const Icon(Icons.document_scanner), + selectedIcon: Icon( + Icons.document_scanner, + color: Theme.of(context).colorScheme.primary, + ), label: S.of(context).bottomNavScannerPageLabel, ), NavigationDestination( - icon: const Icon(Icons.sell), + icon: const Icon( + Icons.sell, + ), + selectedIcon: Icon( + Icons.sell, + color: Theme.of(context).colorScheme.primary, + ), label: S.of(context).bottomNavLabelsPageLabel, ), ], diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index d106cdd..6d595c0 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; +import 'package:paperless_mobile/core/model/paperless_server_information.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; @@ -39,24 +41,46 @@ class InfoDrawer extends StatelessWidget { Text( S.of(context).appTitleText, style: Theme.of(context).textTheme.headline5!.copyWith( - color: - Theme.of(context).colorScheme.onPrimaryContainer), + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), ], ), Align( alignment: Alignment.bottomRight, - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - return Text( - state.authentication?.serverUrl - .replaceAll(RegExp(r'https?://'), "") ?? - "", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer), + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + dense: true, + title: Text( + 'example.paperless.myinstance.com.de', + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + maxLines: 1, + ), + isThreeLine: true, + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Logged in as anton', + textAlign: TextAlign.end, + ), + Text( + '${S.of(context).serverInformationPaperlessVersionText} ${state.version} (API v${state.apiVersion})', + style: Theme.of(context).textTheme.caption, + ), + ], + ), + ), + ], ); }, ), @@ -134,8 +158,8 @@ class InfoDrawer extends StatelessWidget { getIt().reset(); getIt().reset(); getIt().reset(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } }, ), diff --git a/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart b/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart index cebf612..e6a00be 100644 --- a/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart +++ b/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart @@ -38,8 +38,8 @@ class EditCorrespondentPage extends StatelessWidget { ); } Navigator.pop(context); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 395d780..5cf7594 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -59,8 +59,8 @@ class CorrespondentWidget extends StatelessWidget { ); } afterSelected?.call(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/labels/document_type/view/widgets/document_type_widget.dart b/lib/features/labels/document_type/view/widgets/document_type_widget.dart index 6c91c89..9bc35d7 100644 --- a/lib/features/labels/document_type/view/widgets/document_type_widget.dart +++ b/lib/features/labels/document_type/view/widgets/document_type_widget.dart @@ -54,8 +54,8 @@ class DocumentTypeWidget extends StatelessWidget { ); } afterSelected?.call(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart b/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart index b04c6ea..be1a93a 100644 --- a/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart +++ b/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart @@ -43,8 +43,8 @@ class EditStoragePathPage extends StatelessWidget { ); } Navigator.pop(context); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart index 10acece..858097c 100644 --- a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart +++ b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart @@ -58,8 +58,8 @@ class StoragePathWidget extends StatelessWidget { ); } afterSelected?.call(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/labels/tags/view/pages/edit_tag_page.dart b/lib/features/labels/tags/view/pages/edit_tag_page.dart index 8dff9de..dff5d45 100644 --- a/lib/features/labels/tags/view/pages/edit_tag_page.dart +++ b/lib/features/labels/tags/view/pages/edit_tag_page.dart @@ -57,8 +57,8 @@ class EditTagPage extends StatelessWidget { } cubit.updateFilter(filter: updatedFilter); Navigator.pop(context); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/labels/tags/view/widgets/tag_widget.dart b/lib/features/labels/tags/view/widgets/tag_widget.dart index b077d79..a0f367a 100644 --- a/lib/features/labels/tags/view/widgets/tag_widget.dart +++ b/lib/features/labels/tags/view/widgets/tag_widget.dart @@ -28,6 +28,7 @@ class TagWidget extends StatelessWidget { tag.name, style: TextStyle(color: tag.textColor), ), + checkmarkColor: tag.textColor, backgroundColor: tag.color, side: BorderSide.none, ); @@ -57,8 +58,8 @@ class TagWidget extends StatelessWidget { if (afterTagTapped != null) { afterTagTapped!(); } - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/labels/view/pages/edit_label_page.dart b/lib/features/labels/view/pages/edit_label_page.dart index f31bf27..b6bf86e 100644 --- a/lib/features/labels/view/pages/edit_label_page.dart +++ b/lib/features/labels/view/pages/edit_label_page.dart @@ -146,8 +146,8 @@ class _EditLabelPageState extends State> { Navigator.pop(context); } on PaperlessValidationErrors catch (errorMessages) { setState(() => _errors = errorMessages); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } } diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index 809381a..b86c1e7 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -97,10 +97,10 @@ class _LoginPageState extends State { clientCertificate: form[ClientCertificateFormField.fkClientCertificate], ); - } on ErrorMessage catch (error) { - showError(context, error); - } catch (unknownError) { - showSnackBar(context, unknownError.toString()); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); + } catch (unknownError, stackTrace) { + showError(context, ErrorMessage.unknown(), stackTrace); } finally { setState(() => _isLoginLoading = false); } diff --git a/lib/features/scan/view/document_upload_page.dart b/lib/features/scan/view/document_upload_page.dart index cf51e16..68e9659 100644 --- a/lib/features/scan/view/document_upload_page.dart +++ b/lib/features/scan/view/document_upload_page.dart @@ -212,12 +212,12 @@ class _DocumentUploadPageState extends State { showSnackBar(context, S.of(context).documentUploadSuccessText); Navigator.pop(context); widget.afterUpload?.call(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } on PaperlessValidationErrors catch (errorMessages) { setState(() => _errors = errorMessages); - } catch (other) { - showSnackBar(context, other.toString()); + } catch (unknownError, stackTrace) { + showError(context, ErrorMessage.unknown(), stackTrace); } finally { setState(() { _isUploadLoading = false; @@ -233,8 +233,8 @@ class _DocumentUploadPageState extends State { onPressed: () async { try { getIt().reloadDocuments(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } }, label: diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 0c994d5..bd9933c 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -201,8 +201,8 @@ class _ScannerPageState extends State try { BlocProvider.of(context) .removeScan(index); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } }, index: index, @@ -214,8 +214,8 @@ class _ScannerPageState extends State void _reset(BuildContext context) { try { BlocProvider.of(context).reset(); - } on ErrorMessage catch (error) { - showError(context, error); + } on ErrorMessage catch (error, stackTrace) { + showError(context, error, stackTrace); } } diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index d195541..949172c 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -189,5 +189,6 @@ "settingsPageStorageSettingsLabel": "Storage", "settingsPageStorageSettingsDescriptionText": "Manage files and storage space", "documentUpdateErrorMessage": "Document successfully updated.", - "errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat." + "errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat.", + "serverInformationPaperlessVersionText": "Paperless Server-Version" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 451f54b..d58d53f 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -190,5 +190,6 @@ "settingsPageStorageSettingsLabel": "Storage", "settingsPageStorageSettingsDescriptionText": "Manage files and storage space", "documentUpdateErrorMessage": "Document successfully updated.", - "errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate." + "errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate.", + "serverInformationPaperlessVersionText": "Paperless server version" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b22adc7..f18cb1b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,22 +1,22 @@ -import 'dart:developer'; import 'dart:io'; -import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:intl/intl.dart'; +import 'package:intl/intl_standalone.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/global/asset_images.dart'; import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/http_self_signed_certificate_override.dart'; -import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:paperless_mobile/core/util.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; @@ -28,11 +28,6 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; -import 'package:form_builder_validators/form_builder_validators.dart'; -import 'package:intl/intl.dart'; -import 'package:intl/intl_standalone.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; void main() async { @@ -46,29 +41,14 @@ void main() async { configureDependencies(); // Remove temporarily downloaded files. (await FileService.temporaryDirectory).deleteSync(recursive: true); - if (kDebugMode) { - _printDeviceInformation(); - } kPackageInfo = await PackageInfo.fromPlatform(); // Load application settings and stored authentication data getIt().initialize(); await getIt().initialize(); await getIt().initialize(); - // Ogaylesgo runApp(const MyApp()); } -void _printDeviceInformation() async { - final tempPath = await FileService.temporaryDirectory; - log('[DEVICE INFO] Temporary ${tempPath.absolute}'); - final docsPath = await FileService.documentsDirectory; - log('[DEVICE INFO] Documents ${docsPath?.absolute}'); - final downloadPath = await FileService.downloadsDirectory; - log('[DEVICE INFO] Download ${downloadPath?.absolute}'); - final scanPath = await FileService.scanDirectory; - log('[DEVICE INFO] Scan ${scanPath?.absolute}'); -} - class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @@ -83,6 +63,7 @@ class _MyAppState extends State { providers: [ BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), + BlocProvider.value(value: getIt()), ], child: BlocBuilder( bloc: getIt(), @@ -186,7 +167,6 @@ class _AuthenticationWrapperState extends State { super.initState(); // For sharing files coming from outside the app while the app is still opened ReceiveSharingIntent.getMediaStream().listen(handleReceivedFiles); - // For sharing files coming from outside the app while the app is closed ReceiveSharingIntent.getInitialMedia().then(handleReceivedFiles); } diff --git a/lib/util.dart b/lib/util.dart index e6a35cc..1542902 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -7,20 +7,49 @@ import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart' import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:intl/intl.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:paperless_mobile/core/service/github_issue_service.dart'; +import 'package:paperless_mobile/generated/intl/messages_de.dart'; import 'package:path_provider/path_provider.dart'; final dateFormat = DateFormat("yyyy-MM-dd"); final GlobalKey rootScaffoldKey = GlobalKey(); late PackageInfo kPackageInfo; -void showSnackBar(BuildContext context, String message, [String? details]) { +void showSnackBar( + BuildContext context, + String message, { + String? details, + SnackBarAction? action, +}) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message + (details != null ? ' ($details)' : ''))), + SnackBar( + content: Text( + message + (details != null ? ' ($details)' : ''), + ), + action: action, + duration: const Duration(seconds: 5), + ), ); } -void showError(BuildContext context, ErrorMessage error) { - showSnackBar(context, translateError(context, error.code), error.details); +void showError( + BuildContext context, + ErrorMessage error, [ + StackTrace? stackTrace, +]) { + showSnackBar( + context, + translateError(context, error.code), + details: error.details, + action: SnackBarAction( + label: "REPORT", + textColor: Colors.amber, + onPressed: () => GithubIssueService.createIssueFromError( + context, + stackTrace: stackTrace, + ), + ), + ); } bool isNotNull(dynamic value) { diff --git a/pubspec.yaml b/pubspec.yaml index 315159f..8689da9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: equatable: ^2.0.3 flutter_form_builder: ^7.5.0 form_builder_extra_fields: - git: + git: url: https://github.com/flutter-form-builder-ecosystem/form_builder_extra_fields.git ref: main form_builder_validators: ^8.3.0