From aa05f9432aa8b2061cec21f9fe64838cdf4b456b Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 12 Dec 2023 23:00:12 +0100 Subject: [PATCH 1/8] feat: Add notes --- lib/core/bloc/base_state.dart | 25 ++++++ .../view/pages/document_details_page.dart | 42 +++++++++- .../view/widgets/add_note_page.dart | 37 +++++++++ .../widgets/document_meta_data_widget.dart | 83 +++++++++---------- .../view/widgets/document_notes_widget.dart | 38 +++++++++ lib/l10n/intl_ca.arb | 4 +- lib/l10n/intl_cs.arb | 4 +- lib/l10n/intl_de.arb | 4 +- lib/l10n/intl_en.arb | 4 +- lib/l10n/intl_es.arb | 4 +- lib/l10n/intl_fr.arb | 4 +- lib/l10n/intl_nl.arb | 4 +- lib/l10n/intl_pl.arb | 4 +- lib/l10n/intl_ru.arb | 4 +- lib/l10n/intl_tr.arb | 4 +- lib/routing/routes.dart | 1 + lib/routing/routes/documents_route.dart | 14 ++++ .../routes/shells/authenticated_route.dart | 5 ++ .../lib/src/models/document_model.dart | 11 +-- .../lib/src/models/note_model.dart | 17 ++++ 20 files changed, 254 insertions(+), 59 deletions(-) create mode 100644 lib/core/bloc/base_state.dart create mode 100644 lib/features/document_details/view/widgets/add_note_page.dart create mode 100644 lib/features/document_details/view/widgets/document_notes_widget.dart create mode 100644 packages/paperless_api/lib/src/models/note_model.dart diff --git a/lib/core/bloc/base_state.dart b/lib/core/bloc/base_state.dart new file mode 100644 index 0000000..6e16240 --- /dev/null +++ b/lib/core/bloc/base_state.dart @@ -0,0 +1,25 @@ +import 'package:paperless_mobile/core/bloc/loading_status.dart'; + +class BaseState { + final Object? error; + final T? value; + final LoadingStatus status; + + BaseState({ + required this.error, + required this.value, + required this.status, + }); + + BaseState copyWith({ + Object? error, + T? value, + LoadingStatus? status, + }) { + return BaseState( + error: error ?? this.error, + value: value ?? this.value, + status: status ?? this.status, + ); + } +} diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 30ea68c..7d6ccd1 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -15,6 +15,7 @@ import 'package:paperless_mobile/features/document_details/cubit/document_detail import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart'; +import 'package:paperless_mobile/features/document_details/view/widgets/document_notes_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart'; @@ -67,7 +68,7 @@ class _DocumentDetailsPageState extends State { debugPrint(disableAnimations.toString()); final hasMultiUserSupport = context.watch().hasMultiUserSupport; - final tabLength = 4 + (hasMultiUserSupport ? 1 : 0); + final tabLength = 5 + (hasMultiUserSupport ? 1 : 0); return AnnotatedRegion( value: buildOverlayStyle( Theme.of(context), @@ -201,6 +202,16 @@ class _DocumentDetailsPageState extends State { ), ), ), + Tab( + child: Text( + "Notes", + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), if (hasMultiUserSupport) Tab( child: Text( @@ -303,6 +314,35 @@ class _DocumentDetailsPageState extends State { ), ], ), + CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + switch (state.status) { + LoadingStatus.loaded => DocumentNotesWidget( + document: state.document!, + ), + LoadingStatus.error => _buildErrorState(), + _ => _buildLoadingState(), + }, + if (state.status == LoadingStatus.loaded) + SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: () { + AddNoteRoute($extra: state.document!) + .push(context); + }, + icon: Icon(Icons.note_add_outlined), + label: Text('Add note'), + ), + ), + ), + ], + ), if (hasMultiUserSupport) CustomScrollView( controller: _pagingScrollController, diff --git a/lib/features/document_details/view/widgets/add_note_page.dart b/lib/features/document_details/view/widgets/add_note_page.dart new file mode 100644 index 0000000..0de1544 --- /dev/null +++ b/lib/features/document_details/view/widgets/add_note_page.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class AddNotePage extends StatefulWidget { + final DocumentModel document; + + const AddNotePage({super.key, required this.document}); + + @override + State createState() => _AddNotePageState(); +} + +class _AddNotePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(S.of(context)!.addNote), + ), + body: Column( + children: [ + TextField( + decoration: InputDecoration( + labelText: S.of(context)!.content, + ), + ), + ElevatedButton( + onPressed: () {}, + child: Text(S.of(context)!.save), + ), + ], + ), + ); + } +} diff --git a/lib/features/document_details/view/widgets/document_meta_data_widget.dart b/lib/features/document_details/view/widgets/document_meta_data_widget.dart index 02d99fc..d11f402 100644 --- a/lib/features/document_details/view/widgets/document_meta_data_widget.dart +++ b/lib/features/document_details/view/widgets/document_meta_data_widget.dart @@ -25,54 +25,51 @@ class DocumentMetaDataWidget extends StatelessWidget { Widget build(BuildContext context) { final currentUser = context.watch().paperlessUser; - return SliverList( - delegate: SliverChildListDelegate( - [ - if (currentUser.canEditDocuments) - ArchiveSerialNumberField( - document: document, - ).paddedOnly(bottom: itemSpacing), - DetailsItem.text( - DateFormat.yMMMMd(Localizations.localeOf(context).toString()) - .format(document.modified), - context: context, - label: S.of(context)!.modifiedAt, + return SliverList.list( + children: [ + if (currentUser.canEditDocuments) + ArchiveSerialNumberField( + document: document, ).paddedOnly(bottom: itemSpacing), + DetailsItem.text( + DateFormat.yMMMMd(Localizations.localeOf(context).toString()) + .format(document.modified), + context: context, + label: S.of(context)!.modifiedAt, + ).paddedOnly(bottom: itemSpacing), + DetailsItem.text( + DateFormat.yMMMMd(Localizations.localeOf(context).toString()) + .format(document.added), + context: context, + label: S.of(context)!.addedAt, + ).paddedOnly(bottom: itemSpacing), + DetailsItem.text( + metaData.mediaFilename, + context: context, + label: S.of(context)!.mediaFilename, + ).paddedOnly(bottom: itemSpacing), + if (document.originalFileName != null) DetailsItem.text( - DateFormat.yMMMMd(Localizations.localeOf(context).toString()) - .format(document.added), - context: context, - label: S.of(context)!.addedAt, - ).paddedOnly(bottom: itemSpacing), - DetailsItem.text( - metaData.mediaFilename, - context: context, - label: S.of(context)!.mediaFilename, - ).paddedOnly(bottom: itemSpacing), - if (document.originalFileName != null) - DetailsItem.text( - document.originalFileName!, - context: context, - label: S.of(context)!.originalMD5Checksum, - ).paddedOnly(bottom: itemSpacing), - DetailsItem.text( - metaData.originalChecksum, + document.originalFileName!, context: context, label: S.of(context)!.originalMD5Checksum, ).paddedOnly(bottom: itemSpacing), - DetailsItem.text( - formatBytes(metaData.originalSize, 2), - context: context, - label: S.of(context)!.originalFileSize, - ).paddedOnly(bottom: itemSpacing), - DetailsItem.text( - metaData.originalMimeType, - context: context, - label: S.of(context)!.originalMIMEType, - ).paddedOnly(bottom: itemSpacing), - - ], - ), + DetailsItem.text( + metaData.originalChecksum, + context: context, + label: S.of(context)!.originalMD5Checksum, + ).paddedOnly(bottom: itemSpacing), + DetailsItem.text( + formatBytes(metaData.originalSize, 2), + context: context, + label: S.of(context)!.originalFileSize, + ).paddedOnly(bottom: itemSpacing), + DetailsItem.text( + metaData.originalMimeType, + context: context, + label: S.of(context)!.originalMIMEType, + ).paddedOnly(bottom: itemSpacing), + ], ); } } diff --git a/lib/features/document_details/view/widgets/document_notes_widget.dart b/lib/features/document_details/view/widgets/document_notes_widget.dart new file mode 100644 index 0000000..5ffdc77 --- /dev/null +++ b/lib/features/document_details/view/widgets/document_notes_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:paperless_api/paperless_api.dart'; + +class DocumentNotesWidget extends StatelessWidget { + final DocumentModel document; + const DocumentNotesWidget({super.key, required this.document}); + + @override + Widget build(BuildContext context) { + return SliverList.builder( + itemBuilder: (context, index) { + final note = document.notes.elementAt(index); + return ListTile( + title: Text(note.note), + subtitle: Text( + DateFormat.yMMMd(Localizations.localeOf(context).toString()) + .format(note.created)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () {}, + icon: Icon(Icons.edit), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.delete), + ), + ], + ), + ); + }, + itemCount: document.notes.length, + ); + } +} diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb index 1915311..bfb53d6 100644 --- a/lib/l10n/intl_ca.arb +++ b/lib/l10n/intl_ca.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Versió {versionCode}" + "version": "Versió {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 32c55a9..e7b6fbc 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 6118dd3..b314eed 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notizen} one{Notiz} other{Notizen}}", + "addNote": "Notiz hinzufügen" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 84674b1..5a9c1b8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index f05b474..efe3a87 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 471430d..8d02103 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index 84674b1..5a9c1b8 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 4f15c72..fb7fc7d 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 940d32a..5b9ea09 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 98e3675..b84fa77 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1024,5 +1024,7 @@ "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, - "version": "Version {versionCode}" + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note" } \ No newline at end of file diff --git a/lib/routing/routes.dart b/lib/routing/routes.dart index 7f48599..3a44fa1 100644 --- a/lib/routing/routes.dart +++ b/lib/routing/routes.dart @@ -26,4 +26,5 @@ class R { static const loggingOut = "loggingOut"; static const restoringSession = "restoringSession"; static const addAccount = 'addAccount'; + static const addNote = 'addNote'; } diff --git a/lib/routing/routes/documents_route.dart b/lib/routing/routes/documents_route.dart index c26dde7..16ecc98 100644 --- a/lib/routing/routes/documents_route.dart +++ b/lib/routing/routes/documents_route.dart @@ -9,6 +9,7 @@ import 'package:paperless_mobile/features/document_bulk_action/view/widgets/full import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_details/view/widgets/add_note_page.dart'; import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart'; import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; @@ -210,3 +211,16 @@ class BulkEditDocumentsRoute extends GoRouteData { ); } } + +class AddNoteRoute extends GoRouteData { + final DocumentModel $extra; + + AddNoteRoute({required this.$extra}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return AddNotePage( + document: $extra, + ); + } +} diff --git a/lib/routing/routes/shells/authenticated_route.dart b/lib/routing/routes/shells/authenticated_route.dart index feed381..92eac37 100644 --- a/lib/routing/routes/shells/authenticated_route.dart +++ b/lib/routing/routes/shells/authenticated_route.dart @@ -71,6 +71,7 @@ part 'authenticated_route.g.dart'; TypedGoRoute( path: "details/:id", name: R.documentDetails, + routes: [], ), TypedGoRoute( path: "edit", @@ -84,6 +85,10 @@ part 'authenticated_route.g.dart'; path: 'preview', name: R.documentPreview, ), + TypedGoRoute( + path: 'add-note', + name: R.addNote, + ), ], ) ], diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index 3c605ee..b43c58d 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -1,10 +1,9 @@ -// ignore_for_file: non_constant_identifier_names - import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; import 'package:paperless_api/src/models/custom_field_model.dart'; +import 'package:paperless_api/src/models/note_model.dart'; import 'package:paperless_api/src/models/search_hit.dart'; part 'document_model.g.dart'; @@ -48,10 +47,11 @@ class DocumentModel extends Equatable { final int? owner; final bool? userCanChange; + final Iterable notes; - // Only present if full_perms=true + /// Only present if full_perms=true final Permissions? permissions; - final Iterable? customFields; + final Iterable customFields; const DocumentModel({ required this.id, @@ -71,7 +71,8 @@ class DocumentModel extends Equatable { this.owner, this.userCanChange, this.permissions, - this.customFields, + this.customFields = const [], + this.notes = const [], }); factory DocumentModel.fromJson(Map json) => diff --git a/packages/paperless_api/lib/src/models/note_model.dart b/packages/paperless_api/lib/src/models/note_model.dart new file mode 100644 index 0000000..f23c025 --- /dev/null +++ b/packages/paperless_api/lib/src/models/note_model.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'note_model.freezed.dart'; +part 'note_model.g.dart'; + +@freezed +class NoteModel with _$NoteModel { + const factory NoteModel({ + required int id, + required String note, + required DateTime created, + required int document, + required int? user, + }) = _NoteModel; + + factory NoteModel.fromJson(Map json) => + _$NoteModelFromJson(json); +} From d7f297a4dfc7c2d7d2e6ea4ff6e018632967c560 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 19 Dec 2023 20:08:24 +0100 Subject: [PATCH 2/8] feat: Add functionality to delete notes --- lib/core/extensions/flutter_extensions.dart | 11 +- .../cubit/document_details_cubit.dart | 41 ++++ .../view/pages/document_details_page.dart | 219 +++++++++--------- .../view/widgets/document_notes_widget.dart | 92 ++++++-- .../view/widgets/edit_note_page.dart | 15 ++ .../lib/src/models/document_model.dart | 10 + .../paperless_api/lib/src/models/models.dart | 1 + .../lib/src/models/note_model.dart | 8 +- .../paperless_documents_api.dart | 1 + .../paperless_documents_api_impl.dart | 18 ++ scripts/install_dependencies.sh | 23 +- 11 files changed, 288 insertions(+), 151 deletions(-) create mode 100644 lib/features/document_details/view/widgets/edit_note_page.dart diff --git a/lib/core/extensions/flutter_extensions.dart b/lib/core/extensions/flutter_extensions.dart index a4cd2e9..ebbee0c 100644 --- a/lib/core/extensions/flutter_extensions.dart +++ b/lib/core/extensions/flutter_extensions.dart @@ -11,9 +11,18 @@ extension WidgetPadding on Widget { Widget paddedSymmetrically({ double horizontal = 0.0, double vertical = 0.0, + bool sliver = false, }) { + final insets = + EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical); + if (sliver) { + return SliverPadding( + padding: insets, + sliver: this, + ); + } return Padding( - padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), + padding: insets, child: this, ); } diff --git a/lib/features/document_details/cubit/document_details_cubit.dart b/lib/features/document_details/cubit/document_details_cubit.dart index 0238bd6..b010b83 100644 --- a/lib/features/document_details/cubit/document_details_cubit.dart +++ b/lib/features/document_details/cubit/document_details_cubit.dart @@ -87,6 +87,47 @@ class DocumentDetailsCubit extends Cubit { } } + Future updateNote(NoteModel note) async { + assert(state.status == LoadingStatus.loaded); + final document = state.document!; + final updatedNotes = document.notes.map((e) => e.id == note.id ? note : e); + try { + final updatedDocument = await _api.update( + state.document!.copyWith( + notes: updatedNotes, + ), + ); + _notifier.notifyUpdated(updatedDocument); + } on PaperlessApiException catch (e) { + addError( + TransientPaperlessApiError( + code: e.code, + details: e.details, + ), + ); + } + } + + Future deleteNote(NoteModel note) async { + assert(state.status == LoadingStatus.loaded, + "Document data has to be loaded before calling this method."); + assert(note.id != null, "Note id cannot be null."); + try { + final updatedDocument = await _api.deleteNote( + state.document!, + note.id!, + ); + _notifier.notifyUpdated(updatedDocument); + } on PaperlessApiException catch (e) { + addError( + TransientPaperlessApiError( + code: e.code, + details: e.details, + ), + ); + } + } + Future assignAsn( DocumentModel document, { int? asn, diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 7d6ccd1..0052df8 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -240,130 +240,131 @@ class _DocumentDetailsPageState extends State { context.read(), documentId: widget.id, ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 16, - ), - child: TabBarView( - children: [ - CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - switch (state.status) { - LoadingStatus.loaded => - DocumentOverviewWidget( - document: state.document!, - itemSpacing: _itemSpacing, - queryString: - widget.titleAndContentQueryString, - ), - LoadingStatus.error => _buildErrorState(), - _ => _buildLoadingState(), - }, - ], - ), - CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - switch (state.status) { - LoadingStatus.loaded => DocumentContentWidget( - document: state.document!, - queryString: - widget.titleAndContentQueryString, - ), - LoadingStatus.error => _buildErrorState(), - _ => _buildLoadingState(), - } - ], - ), - CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - switch (state.status) { - LoadingStatus.loaded => - DocumentMetaDataWidget( - document: state.document!, - itemSpacing: _itemSpacing, - metaData: state.metaData!, - ), - LoadingStatus.error => _buildErrorState(), - _ => _buildLoadingState(), - }, - ], - ), + child: TabBarView( + children: [ + CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + switch (state.status) { + LoadingStatus.loaded => DocumentOverviewWidget( + document: state.document!, + itemSpacing: _itemSpacing, + queryString: + widget.titleAndContentQueryString, + ).paddedSymmetrically( + vertical: 16, + sliver: true, + ), + LoadingStatus.error => _buildErrorState(), + _ => _buildLoadingState(), + }, + ], + ), + CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + switch (state.status) { + LoadingStatus.loaded => DocumentContentWidget( + document: state.document!, + queryString: + widget.titleAndContentQueryString, + ).paddedSymmetrically( + vertical: 16, + sliver: true, + ), + LoadingStatus.error => _buildErrorState(), + _ => _buildLoadingState(), + } + ], + ), + CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + switch (state.status) { + LoadingStatus.loaded => DocumentMetaDataWidget( + document: state.document!, + itemSpacing: _itemSpacing, + metaData: state.metaData!, + ).paddedSymmetrically( + vertical: 16, + sliver: true, + ), + LoadingStatus.error => _buildErrorState(), + _ => _buildLoadingState(), + }, + ], + ), + CustomScrollView( + controller: _pagingScrollController, + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + SimilarDocumentsView( + pagingScrollController: _pagingScrollController, + ).paddedSymmetrically( + vertical: 16, + sliver: true, + ), + ], + ), + CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + switch (state.status) { + LoadingStatus.loaded => DocumentNotesWidget( + document: state.document!, + ).paddedSymmetrically( + vertical: 16, + sliver: true, + ), + LoadingStatus.error => _buildErrorState(), + _ => _buildLoadingState(), + }, + ], + ), + if (hasMultiUserSupport) CustomScrollView( controller: _pagingScrollController, - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - SimilarDocumentsView( - pagingScrollController: - _pagingScrollController, - ), - ], - ), - CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView .sliverOverlapAbsorberHandleFor(context), ), switch (state.status) { - LoadingStatus.loaded => DocumentNotesWidget( + LoadingStatus.loaded => + DocumentPermissionsWidget( document: state.document!, + ).paddedSymmetrically( + vertical: 16, + sliver: true, ), LoadingStatus.error => _buildErrorState(), _ => _buildLoadingState(), - }, - if (state.status == LoadingStatus.loaded) - SliverToBoxAdapter( - child: Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - onPressed: () { - AddNoteRoute($extra: state.document!) - .push(context); - }, - icon: Icon(Icons.note_add_outlined), - label: Text('Add note'), - ), - ), - ), + } ], ), - if (hasMultiUserSupport) - CustomScrollView( - controller: _pagingScrollController, - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor( - context), - ), - switch (state.status) { - LoadingStatus.loaded => - DocumentPermissionsWidget( - document: state.document!, - ), - LoadingStatus.error => _buildErrorState(), - _ => _buildLoadingState(), - } - ], + ] + .map( + (child) => Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: child, ), - ], - ), + ) + .toList(), ), ); }, diff --git a/lib/features/document_details/view/widgets/document_notes_widget.dart b/lib/features/document_details/view/widgets/document_notes_widget.dart index 5ffdc77..5d472f1 100644 --- a/lib/features/document_details/view/widgets/document_notes_widget.dart +++ b/lib/features/document_details/view/widgets/document_notes_widget.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; class DocumentNotesWidget extends StatelessWidget { final DocumentModel document; @@ -9,30 +14,69 @@ class DocumentNotesWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverList.builder( - itemBuilder: (context, index) { - final note = document.notes.elementAt(index); - return ListTile( - title: Text(note.note), - subtitle: Text( - DateFormat.yMMMd(Localizations.localeOf(context).toString()) - .format(note.created)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () {}, - icon: Icon(Icons.edit), - ), - IconButton( - onPressed: () {}, - icon: Icon(Icons.delete), - ), - ], - ), - ); - }, - itemCount: document.notes.length, + return SliverMainAxisGroup( + slivers: [ + SliverList.separated( + separatorBuilder: (context, index) => const SizedBox(height: 16), + itemBuilder: (context, index) { + final note = document.notes.elementAt(index); + return Card( + // borderRadius: BorderRadius.circular(8), + // elevation: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (note.created != null) + Text( + DateFormat.yMMMd( + Localizations.localeOf(context).toString()) + .addPattern('\u2014') + .add_jm() + .format(note.created!), + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(.5), + ), + ), + const SizedBox(height: 8), + Text( + note.note!, + textAlign: TextAlign.justify, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Spacer(), + IconButton( + icon: Icon(Icons.edit), + onPressed: () { + // Push edit page + }, + ), + IconButton( + icon: Icon(Icons.delete), + onPressed: () { + context.read().deleteNote(note); + showSnackBar( + context, + S.of(context)!.documentSuccessfullyUpdated, + ); + }, + ), + ], + ), + ], + ).padded(16), + ); + }, + itemCount: document.notes.length, + ), + ], ); } } diff --git a/lib/features/document_details/view/widgets/edit_note_page.dart b/lib/features/document_details/view/widgets/edit_note_page.dart new file mode 100644 index 0000000..226600f --- /dev/null +++ b/lib/features/document_details/view/widgets/edit_note_page.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class EditNotePage extends StatefulWidget { + const EditNotePage({super.key}); + + @override + State createState() => _EditNotePageState(); +} + +class _EditNotePageState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} \ No newline at end of file diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index b43c58d..38df520 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; @@ -95,6 +96,9 @@ class DocumentModel extends Equatable { String? archivedFileName, int? Function()? owner, bool? userCanChange, + Iterable? notes, + Permissions? permissions, + Iterable? customFields, }) { return DocumentModel( id: id, @@ -115,6 +119,9 @@ class DocumentModel extends Equatable { archivedFileName: archivedFileName ?? this.archivedFileName, owner: owner != null ? owner() : this.owner, userCanChange: userCanChange ?? this.userCanChange, + customFields: customFields ?? this.customFields, + notes: notes ?? this.notes, + permissions: permissions ?? this.permissions, ); } @@ -135,5 +142,8 @@ class DocumentModel extends Equatable { archivedFileName, owner, userCanChange, + customFields, + notes, + permissions, ]; } diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index e5b8495..77f4147 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -28,3 +28,4 @@ export 'task/task.dart'; export 'task/task_status.dart'; export 'user_model.dart'; export 'exception/exceptions.dart'; +export 'note_model.dart'; diff --git a/packages/paperless_api/lib/src/models/note_model.dart b/packages/paperless_api/lib/src/models/note_model.dart index f23c025..62fda21 100644 --- a/packages/paperless_api/lib/src/models/note_model.dart +++ b/packages/paperless_api/lib/src/models/note_model.dart @@ -5,10 +5,10 @@ part 'note_model.g.dart'; @freezed class NoteModel with _$NoteModel { const factory NoteModel({ - required int id, - required String note, - required DateTime created, - required int document, + required int? id, + required String? note, + required DateTime? created, + required int? document, required int? user, }) = _NoteModel; diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart index 106fef1..2bbb9dc 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart @@ -22,6 +22,7 @@ abstract class PaperlessDocumentsApi { Future find(int id); Future delete(DocumentModel doc); Future getMetaData(int id); + Future deleteNote(DocumentModel document, int noteId); Future> bulkAction(BulkAction action); Future getPreview(int docId); String getThumbnailUrl(int docId); diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index 8cbddea..36cb080 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -323,4 +323,22 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { ); } } + + @override + Future deleteNote(DocumentModel document, int noteId) async { + try { + final response = await client.delete( + "/api/documents/${document.id}/notes/?id=$noteId", + options: Options(validateStatus: (status) => status == 200), + ); + final notes = + (response.data as List).map((e) => NoteModel.fromJson(e)).toList(); + + return document.copyWith(notes: notes); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.documentDeleteFailed), + ); + } + } } diff --git a/scripts/install_dependencies.sh b/scripts/install_dependencies.sh index b85ea79..96a1a87 100755 --- a/scripts/install_dependencies.sh +++ b/scripts/install_dependencies.sh @@ -1,23 +1,20 @@ #!/usr/bin/env bash -set -Eeuo pipefail +set -Euo pipefail __script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) readonly __script_dir pushd "$__script_dir/../" -pushd packages/paperless_api -flutter packages pub get -dart run build_runner build --delete-conflicting-outputs -popd - -pushd packages/mock_server -flutter packages pub get -popd +for dir in packages/*/ # list directories in the form "/tmp/dirname/" +do + pushd $dir + echo "Installing dependencies for $dir" + flutter packages pub get + dart run build_runner build --delete-conflicting-outputs + popd +done flutter packages pub get flutter gen-l10n -dart run build_runner build --delete-conflicting-outputs - -popd - +dart run build_runner build --delete-conflicting-outputs \ No newline at end of file From 55aa42e4abacd8286c77e4916f4e6e031e3f5262 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sun, 31 Dec 2023 15:26:20 +0100 Subject: [PATCH 3/8] feat: Add tests, update notes implementation --- lib/core/model/info_message_exception.dart | 5 ++ lib/core/security/session_manager.dart | 2 +- .../error_code_localization_mapper.dart | 2 + .../cubit/document_details_cubit.dart | 13 +++ .../view/widgets/add_note_page.dart | 37 -------- .../view/widgets/document_notes_widget.dart | 60 ++++++++++--- .../view/widgets/edit_note_page.dart | 15 ---- .../cubit/document_scanner_cubit.dart | 4 +- .../document_scan/view/scanner_page.dart | 3 + lib/routing/routes/documents_route.dart | 14 --- .../routes/shells/authenticated_route.dart | 4 - packages/paperless_api/lib/paperless_api.dart | 1 + .../dio_http_error_interceptor.dart | 0 .../lib/src/models/document_filter.dart | 9 +- .../lib/src/models/filter_rule_model.dart | 1 - .../paperless_api/lib/src/models/models.dart | 2 +- .../lib/src/models/note_model.dart | 14 ++- .../src/models/paperless_api_exception.dart | 15 +++- .../date_range_queries/date_range_query.dart | 9 +- .../query_parameters/id_query_parameter.dart | 9 +- .../tags_query/tags_query.dart | 9 +- .../models/query_parameters/text_query.dart | 6 ++ .../authentication_api.dart | 5 -- .../authentication_api_impl.dart | 5 ++ .../paperless_documents_api.dart | 3 + .../paperless_documents_api_impl.dart | 25 +++++- packages/paperless_api/pubspec.yaml | 2 + .../test/api/authentication/login_test.dart | 90 +++++++++++++++++++ .../test/{ => parsing}/saved_view_test.dart | 24 +++-- 29 files changed, 273 insertions(+), 115 deletions(-) delete mode 100644 lib/features/document_details/view/widgets/add_note_page.dart delete mode 100644 lib/features/document_details/view/widgets/edit_note_page.dart rename {lib/core => packages/paperless_api/lib/src}/interceptor/dio_http_error_interceptor.dart (100%) create mode 100644 packages/paperless_api/test/api/authentication/login_test.dart rename packages/paperless_api/test/{ => parsing}/saved_view_test.dart (94%) diff --git a/lib/core/model/info_message_exception.dart b/lib/core/model/info_message_exception.dart index 817954e..36ddafa 100644 --- a/lib/core/model/info_message_exception.dart +++ b/lib/core/model/info_message_exception.dart @@ -9,4 +9,9 @@ class InfoMessageException implements Exception { this.message, this.stackTrace, }); + + @override + String toString() { + return 'InfoMessageException(code: $code, message: $message, stackTrace: $stackTrace)'; + } } diff --git a/lib/core/security/session_manager.dart b/lib/core/security/session_manager.dart index 2244d34..7e50889 100644 --- a/lib/core/security/session_manager.dart +++ b/lib/core/security/session_manager.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:flutter/material.dart'; -import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart'; +import 'package:paperless_api/src/interceptor/dio_http_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; diff --git a/lib/core/translation/error_code_localization_mapper.dart b/lib/core/translation/error_code_localization_mapper.dart index 499c08e..2fc69f4 100644 --- a/lib/core/translation/error_code_localization_mapper.dart +++ b/lib/core/translation/error_code_localization_mapper.dart @@ -82,5 +82,7 @@ String translateError(BuildContext context, ErrorCode code) { 'Could not load custom field.', //TODO: INTL ErrorCode.customFieldDeleteFailed => 'Could not delete custom field, please try again.', //TODO: INTL + ErrorCode.deleteNoteFailed => 'Could not delete note, please try again.', + ErrorCode.addNoteFailed => 'Could not create note, please try again.', }; } diff --git a/lib/features/document_details/cubit/document_details_cubit.dart b/lib/features/document_details/cubit/document_details_cubit.dart index b010b83..3c17cad 100644 --- a/lib/features/document_details/cubit/document_details_cubit.dart +++ b/lib/features/document_details/cubit/document_details_cubit.dart @@ -311,4 +311,17 @@ class DocumentDetailsCubit extends Cubit { _notifier.removeListener(this); await super.close(); } + + Future addNote(String text) async { + assert(state.status == LoadingStatus.loaded); + try { + final updatedDocument = await _api.addNote( + document: state.document!, + text: text, + ); + _notifier.notifyUpdated(updatedDocument); + } on PaperlessApiException catch (err) { + addError(TransientPaperlessApiError(code: err.code)); + } + } } diff --git a/lib/features/document_details/view/widgets/add_note_page.dart b/lib/features/document_details/view/widgets/add_note_page.dart deleted file mode 100644 index 0de1544..0000000 --- a/lib/features/document_details/view/widgets/add_note_page.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; - -class AddNotePage extends StatefulWidget { - final DocumentModel document; - - const AddNotePage({super.key, required this.document}); - - @override - State createState() => _AddNotePageState(); -} - -class _AddNotePageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(S.of(context)!.addNote), - ), - body: Column( - children: [ - TextField( - decoration: InputDecoration( - labelText: S.of(context)!.content, - ), - ), - ElevatedButton( - onPressed: () {}, - child: Text(S.of(context)!.save), - ), - ], - ), - ); - } -} diff --git a/lib/features/document_details/view/widgets/document_notes_widget.dart b/lib/features/document_details/view/widgets/document_notes_widget.dart index 5d472f1..189e373 100644 --- a/lib/features/document_details/view/widgets/document_notes_widget.dart +++ b/lib/features/document_details/view/widgets/document_notes_widget.dart @@ -8,18 +8,65 @@ import 'package:paperless_mobile/features/document_details/cubit/document_detail import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -class DocumentNotesWidget extends StatelessWidget { +class DocumentNotesWidget extends StatefulWidget { final DocumentModel document; const DocumentNotesWidget({super.key, required this.document}); + @override + State createState() => _DocumentNotesWidgetState(); +} + +class _DocumentNotesWidgetState extends State { + final _noteContentController = TextEditingController(); + final _formKey = GlobalKey(); + @override Widget build(BuildContext context) { return SliverMainAxisGroup( slivers: [ + SliverToBoxAdapter( + child: Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: _noteContentController, + maxLines: null, + validator: (value) { + if (value?.isEmpty ?? true) { + return S.of(context)!.thisFieldIsRequired; + } + return null; + }, + decoration: InputDecoration( + hintText: 'Your note here...', + labelText: 'New note', + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + ).padded(), + Align( + alignment: Alignment.centerRight, + child: FilledButton.icon( + icon: Icon(Icons.note_add_outlined), + label: Text("Add note"), + onPressed: () { + _formKey.currentState?.save(); + if (_formKey.currentState?.validate() ?? false) { + context + .read() + .addNote(_noteContentController.text); + } + }, + ).padded(), + ), + ], + ).padded(), + ), + ), SliverList.separated( separatorBuilder: (context, index) => const SizedBox(height: 16), itemBuilder: (context, index) { - final note = document.notes.elementAt(index); + final note = widget.document.notes.elementAt(index); return Card( // borderRadius: BorderRadius.circular(8), // elevation: 1, @@ -51,13 +98,6 @@ class DocumentNotesWidget extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Spacer(), - IconButton( - icon: Icon(Icons.edit), - onPressed: () { - // Push edit page - }, - ), IconButton( icon: Icon(Icons.delete), onPressed: () { @@ -74,7 +114,7 @@ class DocumentNotesWidget extends StatelessWidget { ).padded(16), ); }, - itemCount: document.notes.length, + itemCount: widget.document.notes.length, ), ], ); diff --git a/lib/features/document_details/view/widgets/edit_note_page.dart b/lib/features/document_details/view/widgets/edit_note_page.dart deleted file mode 100644 index 226600f..0000000 --- a/lib/features/document_details/view/widgets/edit_note_page.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class EditNotePage extends StatefulWidget { - const EditNotePage({super.key}); - - @override - State createState() => _EditNotePageState(); -} - -class _EditNotePageState extends State { - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} \ No newline at end of file diff --git a/lib/features/document_scan/cubit/document_scanner_cubit.dart b/lib/features/document_scan/cubit/document_scanner_cubit.dart index 7cf0c52..dce5108 100644 --- a/lib/features/document_scan/cubit/document_scanner_cubit.dart +++ b/lib/features/document_scan/cubit/document_scanner_cubit.dart @@ -54,7 +54,9 @@ class DocumentScannerCubit extends Cubit { Future removeScan(File file) async { try { - await file.delete(); + if (await file.exists()) { + await file.delete(); + } } catch (error, stackTrace) { throw InfoMessageException( code: ErrorCode.scanRemoveFailed, diff --git a/lib/features/document_scan/view/scanner_page.dart b/lib/features/document_scan/view/scanner_page.dart index 2bf183f..6d63430 100644 --- a/lib/features/document_scan/view/scanner_page.dart +++ b/lib/features/document_scan/view/scanner_page.dart @@ -14,6 +14,7 @@ import 'package:paperless_mobile/core/bloc/loading_status.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/global/constants.dart'; +import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart'; @@ -326,6 +327,8 @@ class _ScannerPageState extends State .removeScan(scans[index]); } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); + } on InfoMessageException catch (error, stackTrace) { + showInfoMessage(context, error, stackTrace); } }, index: index, diff --git a/lib/routing/routes/documents_route.dart b/lib/routing/routes/documents_route.dart index 16ecc98..c26dde7 100644 --- a/lib/routing/routes/documents_route.dart +++ b/lib/routing/routes/documents_route.dart @@ -9,7 +9,6 @@ import 'package:paperless_mobile/features/document_bulk_action/view/widgets/full import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; -import 'package:paperless_mobile/features/document_details/view/widgets/add_note_page.dart'; import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart'; import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; @@ -211,16 +210,3 @@ class BulkEditDocumentsRoute extends GoRouteData { ); } } - -class AddNoteRoute extends GoRouteData { - final DocumentModel $extra; - - AddNoteRoute({required this.$extra}); - - @override - Widget build(BuildContext context, GoRouterState state) { - return AddNotePage( - document: $extra, - ); - } -} diff --git a/lib/routing/routes/shells/authenticated_route.dart b/lib/routing/routes/shells/authenticated_route.dart index 92eac37..9c0f585 100644 --- a/lib/routing/routes/shells/authenticated_route.dart +++ b/lib/routing/routes/shells/authenticated_route.dart @@ -85,10 +85,6 @@ part 'authenticated_route.g.dart'; path: 'preview', name: R.documentPreview, ), - TypedGoRoute( - path: 'add-note', - name: R.addNote, - ), ], ) ], diff --git a/packages/paperless_api/lib/paperless_api.dart b/packages/paperless_api/lib/paperless_api.dart index 444403f..8552f8c 100644 --- a/packages/paperless_api/lib/paperless_api.dart +++ b/packages/paperless_api/lib/paperless_api.dart @@ -4,3 +4,4 @@ export 'src/models/models.dart'; export 'src/modules/modules.dart'; export 'src/converters/converters.dart'; export 'config/hive/hive_type_ids.dart'; +export 'src/interceptor/dio_http_error_interceptor.dart'; diff --git a/lib/core/interceptor/dio_http_error_interceptor.dart b/packages/paperless_api/lib/src/interceptor/dio_http_error_interceptor.dart similarity index 100% rename from lib/core/interceptor/dio_http_error_interceptor.dart rename to packages/paperless_api/lib/src/interceptor/dio_http_error_interceptor.dart diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index fb9d8a9..a2dace3 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -125,8 +125,8 @@ class DocumentFilter extends Equatable { return queryParams; } - @override - String toString() => toQueryParameters().toString(); + // @override + // String toString() => toQueryParameters().toString(); DocumentFilter copyWith({ int? pageSize, @@ -249,9 +249,4 @@ class DocumentFilter extends Equatable { moreLike, selectedView, ]; - - // factory DocumentFilter.fromJson(Map json) => - // _$DocumentFilterFromJson(json); - - // Map toJson() => _$DocumentFilterToJson(this); } diff --git a/packages/paperless_api/lib/src/models/filter_rule_model.dart b/packages/paperless_api/lib/src/models/filter_rule_model.dart index f974351..1f84e55 100644 --- a/packages/paperless_api/lib/src/models/filter_rule_model.dart +++ b/packages/paperless_api/lib/src/models/filter_rule_model.dart @@ -82,7 +82,6 @@ class FilterRule with EquatableMixin { assert(filter.tags is IdsTagsQuery); return filter.copyWith( tags: switch (filter.tags) { - // TODO: Handle this case. IdsTagsQuery(include: var i, exclude: var e) => IdsTagsQuery( include: [...i, int.parse(value!)], exclude: e, diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index 77f4147..fbb67f8 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -28,4 +28,4 @@ export 'task/task.dart'; export 'task/task_status.dart'; export 'user_model.dart'; export 'exception/exceptions.dart'; -export 'note_model.dart'; +export 'note_model.dart' show NoteModel; diff --git a/packages/paperless_api/lib/src/models/note_model.dart b/packages/paperless_api/lib/src/models/note_model.dart index 62fda21..e5ebc6e 100644 --- a/packages/paperless_api/lib/src/models/note_model.dart +++ b/packages/paperless_api/lib/src/models/note_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: invalid_annotation_target + import 'package:freezed_annotation/freezed_annotation.dart'; part 'note_model.freezed.dart'; part 'note_model.g.dart'; @@ -9,9 +11,19 @@ class NoteModel with _$NoteModel { required String? note, required DateTime? created, required int? document, - required int? user, + @JsonKey(fromJson: parseNoteUserFromJson) required int? user, }) = _NoteModel; factory NoteModel.fromJson(Map json) => _$NoteModelFromJson(json); } + +int? parseNoteUserFromJson(dynamic json) { + if (json == null) return null; + if (json is Map) { + return json['id']; + } else if (json is int) { + return json; + } + return null; +} diff --git a/packages/paperless_api/lib/src/models/paperless_api_exception.dart b/packages/paperless_api/lib/src/models/paperless_api_exception.dart index ad60cd0..fde8e10 100644 --- a/packages/paperless_api/lib/src/models/paperless_api_exception.dart +++ b/packages/paperless_api/lib/src/models/paperless_api_exception.dart @@ -11,7 +11,16 @@ class PaperlessApiException implements Exception { this.httpStatusCode, }); - const PaperlessApiException.unknown() : this(ErrorCode.unknown); + const PaperlessApiException.unknown({ + String? details, + StackTrace? stackTrace, + int? httpStatusCode, + }) : this( + ErrorCode.unknown, + details: details, + stackTrace: stackTrace, + httpStatusCode: httpStatusCode, + ); @override String toString() { @@ -71,5 +80,7 @@ enum ErrorCode { updateSavedViewError, customFieldCreateFailed, customFieldLoadFailed, - customFieldDeleteFailed; + customFieldDeleteFailed, + deleteNoteFailed, + addNoteFailed; } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart index 3396aa2..d232aba 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart @@ -11,7 +11,7 @@ import 'date_range_unit.dart'; part 'date_range_query.g.dart'; -sealed class DateRangeQuery { +sealed class DateRangeQuery with EquatableMixin { const DateRangeQuery(); Map toQueryParameter(DateRangeQueryField field); @@ -28,10 +28,13 @@ class UnsetDateRangeQuery extends DateRangeQuery { @override bool matches(DateTime dt) => true; + + @override + List get props => []; } @HiveType(typeId: PaperlessApiHiveTypeIds.relativeDateRangeQuery) -class RelativeDateRangeQuery extends DateRangeQuery with EquatableMixin { +class RelativeDateRangeQuery extends DateRangeQuery { @HiveField(0) final int offset; @HiveField(1) @@ -84,7 +87,7 @@ class RelativeDateRangeQuery extends DateRangeQuery with EquatableMixin { @JsonSerializable() @HiveType(typeId: PaperlessApiHiveTypeIds.absoluteDateRangeQuery) -class AbsoluteDateRangeQuery extends DateRangeQuery with EquatableMixin { +class AbsoluteDateRangeQuery extends DateRangeQuery { @LocalDateTimeJsonConverter() @HiveField(0) final DateTime? after; diff --git a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart index 6db3cd5..4b0a694 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart @@ -4,7 +4,7 @@ import 'package:paperless_api/config/hive/hive_type_ids.dart'; part 'id_query_parameter.g.dart'; -sealed class IdQueryParameter { +sealed class IdQueryParameter with EquatableMixin { const IdQueryParameter(); Map toQueryParameter(String field); bool matches(int? id); @@ -23,6 +23,9 @@ class UnsetIdQueryParameter extends IdQueryParameter { @override bool matches(int? id) => true; + + @override + List get props => []; } // @HiveType(typeId: PaperlessApiHiveTypeIds.notAssignedIdQueryParameter) @@ -36,6 +39,8 @@ class NotAssignedIdQueryParameter extends IdQueryParameter { @override bool matches(int? id) => id == null; + @override + List get props => []; } // @HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedIdQueryParameter) @@ -48,6 +53,8 @@ class AnyAssignedIdQueryParameter extends IdQueryParameter { @override bool matches(int? id) => id != null; + @override + List get props => []; } @HiveType(typeId: PaperlessApiHiveTypeIds.setIdQueryParameter) diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart index cfa0187..438c999 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart @@ -4,7 +4,7 @@ import 'package:paperless_api/config/hive/hive_type_ids.dart'; part 'tags_query.g.dart'; -sealed class TagsQuery { +sealed class TagsQuery with EquatableMixin { const TagsQuery(); Map toQueryParameter(); bool matches(Iterable ids); @@ -20,10 +20,13 @@ class NotAssignedTagsQuery extends TagsQuery { @override bool matches(Iterable ids) => ids.isEmpty; + + @override + List get props => []; } @HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedTagsQuery) -class AnyAssignedTagsQuery extends TagsQuery with EquatableMixin { +class AnyAssignedTagsQuery extends TagsQuery { @HiveField(0) final List tagIds; const AnyAssignedTagsQuery({ @@ -54,7 +57,7 @@ class AnyAssignedTagsQuery extends TagsQuery with EquatableMixin { } @HiveType(typeId: PaperlessApiHiveTypeIds.idsTagsQuery) -class IdsTagsQuery extends TagsQuery with EquatableMixin { +class IdsTagsQuery extends TagsQuery { @HiveField(0) final List include; @HiveField(1) diff --git a/packages/paperless_api/lib/src/models/query_parameters/text_query.dart b/packages/paperless_api/lib/src/models/query_parameters/text_query.dart index 7c31e68..b5716f1 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/text_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/text_query.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/config/hive/hive_type_ids.dart'; @@ -91,6 +92,11 @@ class TextQuery { return other.queryText == queryText && other.queryType == queryType; } + @override + String toString() { + return "TextQuery($queryText, $queryType)"; + } + @override int get hashCode => Object.hash(queryText, queryType); } diff --git a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart index 7735979..f1d685e 100644 --- a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart +++ b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api.dart @@ -1,9 +1,4 @@ -import 'package:paperless_api/src/models/exception/exceptions.dart'; - abstract class PaperlessAuthenticationApi { - /// - /// @throws [PaperlessUnauthorizedException] - /// Future login({ required String username, required String password, diff --git a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart index 3ca527a..ecccb06 100644 --- a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart @@ -37,6 +37,11 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { // return AuthenticationTemporaryRedirect(redirectUrl!); } on DioException catch (exception) { throw exception.unravel(); + } catch (error, stackTrace) { + throw PaperlessApiException.unknown( + details: error.toString(), + stackTrace: stackTrace, + ); } } } diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart index 2bbb9dc..37a228d 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart @@ -36,4 +36,7 @@ abstract class PaperlessDocumentsApi { Future findSuggestions(DocumentModel document); Future> autocomplete(String query, [int limit = 10]); + + Future addNote( + {required DocumentModel document, required String text}); } diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index 36cb080..5b746cc 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -337,7 +337,30 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { return document.copyWith(notes: notes); } on DioException catch (exception) { throw exception.unravel( - orElse: const PaperlessApiException(ErrorCode.documentDeleteFailed), + orElse: const PaperlessApiException(ErrorCode.deleteNoteFailed), + ); + } + } + + @override + Future addNote({ + required DocumentModel document, + required String text, + }) async { + try { + final response = await client.post( + "/api/documents/${document.id}/notes/", + options: Options(validateStatus: (status) => status == 200), + data: {'note': text}, + ); + + final notes = + (response.data as List).map((e) => NoteModel.fromJson(e)).toList(); + + return document.copyWith(notes: notes); + } on DioException catch (exception) { + throw exception.unravel( + orElse: const PaperlessApiException(ErrorCode.addNoteFailed), ); } } diff --git a/packages/paperless_api/pubspec.yaml b/packages/paperless_api/pubspec.yaml index 2ae45da..259568d 100644 --- a/packages/paperless_api/pubspec.yaml +++ b/packages/paperless_api/pubspec.yaml @@ -22,6 +22,8 @@ dependencies: jiffy: ^5.0.0 freezed_annotation: ^2.4.1 hive: ^2.2.3 + mockito: ^5.4.4 + http_mock_adapter: ^0.6.1 dev_dependencies: flutter_test: diff --git a/packages/paperless_api/test/api/authentication/login_test.dart b/packages/paperless_api/test/api/authentication/login_test.dart new file mode 100644 index 0000000..776c7bf --- /dev/null +++ b/packages/paperless_api/test/api/authentication/login_test.dart @@ -0,0 +1,90 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http_mock_adapter/http_mock_adapter.dart'; +import 'package:mockito/mockito.dart'; +import 'package:paperless_api/paperless_api.dart'; + +void main() { + group('AuthenticationApi with DioHttpErrorIncerceptor', () { + late PaperlessAuthenticationApi authenticationApi; + late DioAdapter mockAdapter; + const token = "abcde"; + const invalidCredentialsServerMessage = + "Unable to log in with provided credentials."; + + setUp(() { + final dio = Dio()..interceptors.add(DioHttpErrorInterceptor()); + authenticationApi = PaperlessAuthenticationApiImpl(dio); + mockAdapter = DioAdapter(dio: dio); + // Valid credentials + mockAdapter.onPost( + "/api/token/", + data: { + "username": "username", + "password": "password", + }, + (server) => server.reply(200, {"token": token}), + ); + // Invalid credentials + mockAdapter.onPost( + "/api/token/", + data: { + "username": "wrongUsername", + "password": "wrongPassword", + }, + (server) => server.reply(400, { + "non_field_errors": [invalidCredentialsServerMessage] + }), + ); + }); + + // tearDown(() {}); + test( + 'should return a valid token when logging in with valid credentials', + () { + expect( + authenticationApi.login( + username: "username", + password: "password", + ), + completion(token), + ); + }, + ); + + test( + 'should throw a PaperlessFormValidationException containing a reason ' + 'when logging in with invalid credentials', + () { + expect( + authenticationApi.login( + username: "wrongUsername", + password: "wrongPassword", + ), + throwsA(isA().having( + (e) => e.unspecificErrorMessage(), + "non-field specific error message", + equals(invalidCredentialsServerMessage), + )), + ); + }, + ); + + test( + 'should return an error when logging in with invalid credentials', + () { + expect( + authenticationApi.login( + username: "wrongUsername", + password: "wrongPassword", + ), + throwsA(isA().having( + (e) => e.unspecificErrorMessage(), + "non-field specific error message", + equals(invalidCredentialsServerMessage), + )), + ); + }, + ); + }); +} diff --git a/packages/paperless_api/test/saved_view_test.dart b/packages/paperless_api/test/parsing/saved_view_test.dart similarity index 94% rename from packages/paperless_api/test/saved_view_test.dart rename to packages/paperless_api/test/parsing/saved_view_test.dart index 0e1abf0..63a0c39 100644 --- a/packages/paperless_api/test/saved_view_test.dart +++ b/packages/paperless_api/test/parsing/saved_view_test.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:paperless_api/paperless_api.dart'; void main() { - group('Validate parsing logic from [SavedView] to [DocumentFilter]:', () { + group('Parsing [SavedView] to [DocumentFilter]:', () { test('Values are correctly parsed if set.', () { expect( SavedView.fromJson({ @@ -64,7 +64,7 @@ void main() { ] }).toDocumentFilter(), equals( - DocumentFilter.initial.copyWith( + DocumentFilter( correspondent: const SetIdQueryParameter(id: 42), documentType: const SetIdQueryParameter(id: 69), storagePath: const SetIdQueryParameter(id: 14), @@ -83,6 +83,7 @@ void main() { sortField: SortField.created, sortOrder: SortOrder.descending, query: const TextQuery.extended("Never gonna give you up"), + selectedView: 1, ), ), ); @@ -99,7 +100,11 @@ void main() { "sort_reverse": true, "filter_rules": [], }).toDocumentFilter(), - equals(DocumentFilter.initial), + equals( + const DocumentFilter( + selectedView: 1, + ), + ), ); }); @@ -130,11 +135,12 @@ void main() { }, ], }).toDocumentFilter(); - final expected = DocumentFilter.initial.copyWith( - correspondent: const NotAssignedIdQueryParameter(), - documentType: const NotAssignedIdQueryParameter(), - storagePath: const NotAssignedIdQueryParameter(), - tags: const NotAssignedTagsQuery(), + const expected = DocumentFilter( + correspondent: NotAssignedIdQueryParameter(), + documentType: NotAssignedIdQueryParameter(), + storagePath: NotAssignedIdQueryParameter(), + tags: NotAssignedTagsQuery(), + selectedView: 1, ); expect( actual, @@ -148,6 +154,7 @@ void main() { expect( SavedView.fromDocumentFilter( DocumentFilter( + selectedView: 1, correspondent: const SetIdQueryParameter(id: 1), documentType: const SetIdQueryParameter(id: 2), storagePath: const SetIdQueryParameter(id: 3), @@ -173,6 +180,7 @@ void main() { ), equals( SavedView( + id: 1, name: "test_name", showOnDashboard: false, showInSidebar: false, From 9adfefc0f566190c55c8b97db52e3a82f1513993 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Wed, 3 Jan 2024 14:43:03 +0100 Subject: [PATCH 4/8] feat: Update translations, fix reading current server version --- lib/features/settings/view/settings_page.dart | 53 +- .../widgets/language_selection_setting.dart | 1 + lib/l10n/intl_ca.arb | 5 +- lib/l10n/intl_cs.arb | 3 +- lib/l10n/intl_de.arb | 3 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_es.arb | 7 +- lib/l10n/intl_fr.arb | 119 +- lib/l10n/intl_it.arb | 1031 +++++++++++++++++ lib/l10n/intl_nl.arb | 3 +- lib/l10n/intl_pl.arb | 3 +- lib/l10n/intl_ro.arb | 1031 +++++++++++++++++ lib/l10n/intl_ru.arb | 3 +- lib/l10n/intl_tr.arb | 3 +- lib/main.dart | 1 + .../paperless_server_information_model.dart | 5 +- .../paperless_server_stats_api_impl.dart | 12 +- 17 files changed, 2201 insertions(+), 85 deletions(-) create mode 100644 lib/l10n/intl_it.arb create mode 100644 lib/l10n/intl_ro.arb diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index 2b43ca8..872a30e 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -1,3 +1,5 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/settings/view/widgets/app_logs_tile.dart'; @@ -15,6 +17,7 @@ import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setti import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -80,15 +83,49 @@ class SettingsPage extends StatelessWidget { ); } final serverData = snapshot.data!; - return Text( - S.of(context)!.paperlessServerVersion + - ' ' + - serverData.version.toString() + - ' (API v${serverData.apiVersion})', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Theme.of(context).colorScheme.secondary, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + S.of(context)!.paperlessServerVersion + + ' ' + + serverData.version.toString() + + ' (API v${serverData.apiVersion})', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + textAlign: TextAlign.center, + ), + if (serverData.isUpdateAvailable) ...[ + SizedBox(height: 8), + Text.rich( + TextSpan( + style: Theme.of(context).textTheme.labelSmall!, + text: '${S.of(context)!.newerVersionAvailable} ', + children: [ + TextSpan( + text: serverData.latestVersion, + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith( + decoration: TextDecoration.underline, + color: CupertinoColors.link, + decorationColor: CupertinoColors.link, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrlString( + "https://github.com/paperless-ngx/paperless-ngx/releases/tag/${serverData.latestVersion}", + ); + }, + ), + ], + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, + ] + ], ); }, ), diff --git a/lib/features/settings/view/widgets/language_selection_setting.dart b/lib/features/settings/view/widgets/language_selection_setting.dart index cb18576..b742e40 100644 --- a/lib/features/settings/view/widgets/language_selection_setting.dart +++ b/lib/features/settings/view/widgets/language_selection_setting.dart @@ -23,6 +23,7 @@ class _LanguageSelectionSettingState extends State { 'pl': LanguageOption('Polska', true), 'ca': LanguageOption('Català', true), 'ru': LanguageOption('Русский', true), + 'it': LanguageOption('Italiano', true), }; @override diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb index bfb53d6..1fbf9eb 100644 --- a/lib/l10n/intl_ca.arb +++ b/lib/l10n/intl_ca.arb @@ -1025,6 +1025,7 @@ "description": "Text shown while the app tries to establish a connection to the specified host." }, "version": "Versió {versionCode}", - "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "notes": "{count, plural, zero{Notes} one{Nota} other{Notes}}", + "addNote": "Afegir Nota", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index e7b6fbc..3a9ddb7 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -1026,5 +1026,6 @@ }, "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "addNote": "Add note", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index b314eed..35f699e 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1026,5 +1026,6 @@ }, "version": "Version {versionCode}", "notes": "{count, plural, zero{Notizen} one{Notiz} other{Notizen}}", - "addNote": "Notiz hinzufügen" + "addNote": "Notiz hinzufügen", + "newerVersionAvailable": "Neuere Version verfügbar:" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5a9c1b8..fab84f1 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1026,5 +1026,6 @@ }, "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "addNote": "Add note", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index efe3a87..63ab94a 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1020,11 +1020,12 @@ }, "misc": "Otros", "loggingOut": "Cerrando sesión...", - "testingConnection": "Testing connection...", + "testingConnection": "Probando conexión...", "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, "version": "Version {versionCode}", - "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "notes": "{count, plural, zero{Notas} one{Nota} other{Notas}}", + "addNote": "Añadir nota", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 8d02103..be1d4a1 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -703,7 +703,7 @@ "@confirmAction": { "description": "Typically used as a title to confirm a previously selected action" }, - "areYouSureYouWantToContinue": "Etes-vous sûr(e) de vouloir continuer?", + "areYouSureYouWantToContinue": "Êtes-vous sûr(e) de vouloir continuer ?", "bulkEditTagsAddMessage": "{count, plural, one{Cette opération va ajouter les balises {tags} au document sélectionné} other{Cette opération va ajouter les balises {tags} à {count} documents sélectionnés!}}", "@bulkEditTagsAddMessage": { "description": "Message of the confirmation dialog when bulk adding tags." @@ -717,7 +717,7 @@ "description": "Message of the confirmation dialog when both adding and removing tags." }, "bulkEditCorrespondentAssignMessage": "{count, plural, one{Cette opération assignera le correspondant {correspondent} au document sélectionné} other{Cette opération va assigner le correspondant {correspondent} à {count} documents sélectionnés!}}", - "bulkEditDocumentTypeAssignMessage": "{count, plural, one{Cette opération assignera le type de document {docType} au document sélectionné.} other{Cette opération va assigner le documentType {docType} à {count} documents sélectionnés.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{Cette opération assignera le type de document {docType} au document sélectionné.} other{Cette opération va assigner le type de document {docType} à {count} documents sélectionnés.}}", "bulkEditStoragePathAssignMessage": "{count, plural, one{Cette opération assignera le chemin de stockage {path} au document sélectionné.} other{Cette opération va assigner le chemin de stockage {path} à {count} documents sélectionnés.}}", "bulkEditCorrespondentRemoveMessage": "{count, plural, one{Cette opération va supprimer le correspondant du document sélectionné.} other{Cette opération va supprimer le correspondant de {count} documents sélectionnés.}}", "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{Cette opération va supprimer le type de document du document sélectionné.} other{Cette opération va supprimer le type de document de {count} documents sélectionnés.}}", @@ -772,7 +772,7 @@ "@defaultDownloadFileType": { "description": "Label indicating the default filetype to download (one of archived, original and always ask)" }, - "defaultShareFileType": "Type de fichier par défaut de partage", + "defaultShareFileType": "Type de fichier par défaut pour le partage", "@defaultShareFileType": { "description": "Label indicating the default filetype to share (one of archived, original and always ask)" }, @@ -861,7 +861,7 @@ "@loginRequiredPermissionsHint": { "description": "Hint shown on the login page informing the user of the required permissions to use the app." }, - "missingPermissions": "You do not have the necessary permissions to perform this action.", + "missingPermissions": "Vous n'avez pas les permissions nécessaires pour faire cette action.", "@missingPermissions": { "description": "Message shown in a snackbar when a user without the reequired permissions performs an action." }, @@ -873,158 +873,159 @@ "@donate": { "description": "Label of the in-app donate button" }, - "donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!", + "donationDialogContent": "Merci d'avoir envisagé de soutenir cette application ! En raison des politiques de paiement de Google et d'Apple, aucun lien menant aux dons ne peut être affiché dans l'application. Même un lien vers la page du dépôt du projet ne semble pas autorisé dans ce contexte. Par conséquent, jetez peut-être un coup d'oeil à la section « Donations » dans le README du projet. Votre soutien est très apprécié et maintient en vie le développement de cette application. Merci !", "@donationDialogContent": { "description": "Text displayed in the donation dialog" }, - "noDocumentsFound": "No documents found.", + "noDocumentsFound": "Aucun document trouvé.", "@noDocumentsFound": { "description": "Message shown when no documents were found." }, - "couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.", + "couldNotDeleteCorrespondent": "Impossible de supprimer le correspondant, veuillez réessayer.", "@couldNotDeleteCorrespondent": { "description": "Message shown in snackbar when a correspondent could not be deleted." }, - "couldNotDeleteDocumentType": "Could not delete document type, please try again.", + "couldNotDeleteDocumentType": "Impossible de supprimer ce type de document, veuillez réessayer.", "@couldNotDeleteDocumentType": { "description": "Message shown when a document type could not be deleted" }, - "couldNotDeleteTag": "Could not delete tag, please try again.", + "couldNotDeleteTag": "Impossible de supprimer l'étiquette, veuillez réessayer.", "@couldNotDeleteTag": { "description": "Message shown when a tag could not be deleted" }, - "couldNotDeleteStoragePath": "Could not delete storage path, please try again.", + "couldNotDeleteStoragePath": "Impossible de supprimer le chemin de stockage, veuillez réessayer.", "@couldNotDeleteStoragePath": { "description": "Message shown when a storage path could not be deleted" }, - "couldNotUpdateCorrespondent": "Could not update correspondent, please try again.", + "couldNotUpdateCorrespondent": "Impossible de mettre à jour le correspondant, veuillez réessayer.", "@couldNotUpdateCorrespondent": { "description": "Message shown when a correspondent could not be updated" }, - "couldNotUpdateDocumentType": "Could not update document type, please try again.", + "couldNotUpdateDocumentType": "Impossible de mettre à jour le type de document, veuillez réessayer.", "@couldNotUpdateDocumentType": { "description": "Message shown when a document type could not be updated" }, - "couldNotUpdateTag": "Could not update tag, please try again.", + "couldNotUpdateTag": "Impossible de mettre à jour l'étiquette, veuillez réessayer.", "@couldNotUpdateTag": { "description": "Message shown when a tag could not be updated" }, - "couldNotLoadServerInformation": "Could not load server information.", + "couldNotLoadServerInformation": "Impossible de charger les informations du serveur.", "@couldNotLoadServerInformation": { "description": "Message shown when the server information could not be loaded" }, - "couldNotLoadStatistics": "Could not load server statistics.", + "couldNotLoadStatistics": "Impossible de charger les statistiques du serveur.", "@couldNotLoadStatistics": { "description": "Message shown when the server statistics could not be loaded" }, - "couldNotLoadUISettings": "Could not load UI settings.", + "couldNotLoadUISettings": "Impossible de charger les paramètres de l'interface.", "@couldNotLoadUISettings": { "description": "Message shown when the UI settings could not be loaded" }, - "couldNotLoadTasks": "Could not load tasks.", + "couldNotLoadTasks": "Impossible de charger les tâches.", "@couldNotLoadTasks": { "description": "Message shown when the tasks (e.g. document consumed) could not be loaded" }, - "userNotFound": "User could not be found.", + "userNotFound": "L'utilisateur ne peut pas être trouvé.", "@userNotFound": { "description": "Message shown when the specified user (e.g. by id) could not be found" }, - "couldNotUpdateSavedView": "Could not update saved view, please try again.", + "couldNotUpdateSavedView": "Impossible de mettre à jour la vue, veuillez réessayer.", "@couldNotUpdateSavedView": { "description": "Message shown when a saved view could not be updated" }, - "couldNotUpdateStoragePath": "Could not update storage path, please try again.", - "savedViewSuccessfullyUpdated": "Saved view successfully updated.", + "couldNotUpdateStoragePath": "Impossible de mettre à jour le chemin de stockage, veuillez réessayer.", + "savedViewSuccessfullyUpdated": "Vue enregistrée mise à jour avec succès.", "@savedViewSuccessfullyUpdated": { "description": "Message shown when a saved view was successfully updated." }, - "discardChanges": "Discard changes?", + "discardChanges": "Annuler les modifications ?", "@discardChanges": { "description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes." }, - "savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?", + "savedViewChangedDialogContent": "Les conditions de filtre de la vue active ont changé. En réinitialisant le filtre, ces modifications seront perdues. Voulez-vous continuer ?", "@savedViewChangedDialogContent": { "description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view." }, - "createFromCurrentFilter": "Create from current filter", + "createFromCurrentFilter": "Créer à partir du filtre actuel", "@createFromCurrentFilter": { "description": "Tooltip of the \"New saved view\" button" }, - "home": "Home", + "home": "Accueil", "@home": { "description": "Label of the \"Home\" route" }, - "welcomeUser": "Welcome, {name}!", + "welcomeUser": "Bienvenue {name} !", "@welcomeUser": { "description": "Top message shown on the home page" }, - "statistics": "Statistics", - "documentsInInbox": "Documents in inbox", - "totalDocuments": "Total documents", - "totalCharacters": "Total characters", - "showAll": "Show all", + "statistics": "Statistiques", + "documentsInInbox": "Documents dans la boîte de réception", + "totalDocuments": "Nombre total de documents", + "totalCharacters": "Nombre total de caractères", + "showAll": "Tout afficher", "@showAll": { "description": "Button label shown on a saved view preview to open this view in the documents page" }, - "userAlreadyExists": "This user already exists.", + "userAlreadyExists": "Cet utilisateur existe déjà.", "@userAlreadyExists": { "description": "Error message shown when the user tries to add an already existing account." }, - "youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.", + "youDidNotSaveAnyViewsYet": "Vous n'avez pas encore enregistré de vues, créez en une et elle sera affichée ici.", "@youDidNotSaveAnyViewsYet": { "description": "Message shown when there are no saved views yet." }, - "tryAgain": "Try again", - "discardFile": "Discard file?", - "discard": "Discard", - "backToLogin": "Back to login", - "skipEditingReceivedFiles": "Skip editing received files", - "uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.", - "authenticatingDots": "Authenticating...", + "tryAgain": "Veuillez réessayer", + "discardFile": "Abandonner le fichier ?", + "discard": "Abandonner", + "backToLogin": "Retour à la page de connexion", + "skipEditingReceivedFiles": "Passer l'édition des fichiers reçus", + "uploadWithoutPromptingUploadForm": "Toujours mettre en ligne sans montrer le formulaire de mise en ligne lors du partage de fichiers avec l'application.", + "authenticatingDots": "Authentification en cours...", "@authenticatingDots": { "description": "Message shown when the app is authenticating the user" }, - "persistingUserInformation": "Persisting user information...", - "fetchingUserInformation": "Fetching user information...", + "persistingUserInformation": "Sauvegarde des informations utilisateur...", + "fetchingUserInformation": "Récupération des informations utilisateur...", "@fetchingUserInformation": { "description": "Message shown when the app loads user data from the server" }, - "restoringSession": "Restoring session...", + "restoringSession": "Restauration de la session...", "@restoringSession": { "description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in" }, - "documentsAssigned": "{count, plural, zero{No documents} one{1 document} other{{count} documents}}", + "documentsAssigned": "{count, plural, zero{Pas de document} one{1 document} other{{count} documents}}", "@documentsAssigned": { "description": "Text shown with a correspondent, document type etc. to indicate the number of documents this filter will maximally yield." }, - "discardChangesWarning": "You have unsaved changes. By continuing, all changes will be lost. Do you want to discard these changes?", + "discardChangesWarning": "Vous avez des modifications non enregistrées. En continuant, toutes les modifications seront perdues. Voulez-vous abandonner ces modifications ?", "@discardChangesWarning": { "description": "Warning message shown when the user tries to close a route without saving the changes." }, - "changelog": "Changelog", - "noLogsFoundOn": "No logs found on {date}.", - "logfileBottomReached": "You have reached the bottom of this logfile.", - "appLogs": "App logs {date}", - "saveLogsToFile": "Save logs to file", - "copyToClipboard": "Copy to clipboard", - "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", - "loadingLogsFrom": "Loading logs from {date}...", - "clearLogs": "Clear logs from {date}", - "showPdf": "Show PDF", + "changelog": "Notes de version", + "noLogsFoundOn": "Aucun journal trouvé sur {date}.", + "logfileBottomReached": "Vous avez atteint le bas de ce fichier journal.", + "appLogs": "Journaux d'application {date}", + "saveLogsToFile": "Enregistrer le fichier journal", + "copyToClipboard": "Copier dans le presse-papier", + "couldNotLoadLogfileFrom": "Impossible de charger le fichier journal depuis {date}.", + "loadingLogsFrom": "Chargement des journaux depuis {date}...", + "clearLogs": "Effacer les journaux de {date}", + "showPdf": "Afficher le PDF", "@showPdf": { "description": "Tooltip shown on the \"show pdf\" button on the document edit page" }, - "hidePdf": "Hide PDF", + "hidePdf": "Masquer le PDF", "@hidePdf": { "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" }, "misc": "Sonstige", - "loggingOut": "Logging out...", - "testingConnection": "Testing connection...", + "loggingOut": "Déconnexion...", + "testingConnection": "Vérifier la connexion...", "@testingConnection": { "description": "Text shown while the app tries to establish a connection to the specified host." }, "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "addNote": "Ajouter une note", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb new file mode 100644 index 0000000..a46990c --- /dev/null +++ b/lib/l10n/intl_it.arb @@ -0,0 +1,1031 @@ +{ + "developedBy": "Sviluppato da {name}.", + "@developedBy": { + "placeholders": { + "name": {} + } + }, + "addAnotherAccount": "Aggiungi un altro account", + "@addAnotherAccount": {}, + "account": "Account", + "@account": {}, + "addCorrespondent": "Nuovo Corrispondente", + "@addCorrespondent": { + "description": "Title when adding a new correspondent" + }, + "addDocumentType": "Nuovo Tipo Di Documento", + "@addDocumentType": { + "description": "Title when adding a new document type" + }, + "addStoragePath": "Nuovo Percorso Di Archiviazione", + "@addStoragePath": { + "description": "Title when adding a new storage path" + }, + "addTag": "Nuovo Tag", + "@addTag": { + "description": "Title when adding a new tag" + }, + "aboutThisApp": "Info sull'app", + "@aboutThisApp": { + "description": "Label for about this app tile displayed in the drawer" + }, + "loggedInAs": "Accesso effettuato come {name}", + "@loggedInAs": { + "placeholders": { + "name": {} + } + }, + "disconnect": "Disconnetti", + "@disconnect": { + "description": "Logout button label" + }, + "reportABug": "Segnala un Bug", + "@reportABug": {}, + "settings": "Impostazioni", + "@settings": {}, + "authenticateOnAppStart": "Autenticarsi all'avvio dell'app", + "@authenticateOnAppStart": { + "description": "Description of the biometric authentication settings tile" + }, + "biometricAuthentication": "Autenticazione biometrica", + "@biometricAuthentication": {}, + "authenticateToToggleBiometricAuthentication": "{mode, select, enable{Autenticati per abilitare l'autenticazione biometrica} disable{Autenticati per disabilitare l'autenticazione biometrica} other{}}", + "@authenticateToToggleBiometricAuthentication": { + "placeholders": { + "mode": {} + } + }, + "documents": "Documenti", + "@documents": {}, + "inbox": "Posta in arrivo", + "@inbox": {}, + "labels": "Etichette", + "@labels": {}, + "scanner": "Scansiona", + "@scanner": {}, + "startTyping": "Inizia a scrivere...", + "@startTyping": {}, + "doYouReallyWantToDeleteThisView": "Sei sicuro di voler cancellare questa vista?", + "@doYouReallyWantToDeleteThisView": {}, + "deleteView": "Cancellare vista {name}?", + "@deleteView": {}, + "addedAt": "Aggiunto il", + "@addedAt": {}, + "archiveSerialNumber": "Numero seriale di archivio", + "@archiveSerialNumber": {}, + "asn": "ASN", + "@asn": {}, + "correspondent": "Corrispondente", + "@correspondent": {}, + "createdAt": "Creato il", + "@createdAt": {}, + "documentSuccessfullyDeleted": "Documento cancellato con successo.", + "@documentSuccessfullyDeleted": {}, + "assignAsn": "Assegna ASN", + "@assignAsn": {}, + "deleteDocumentTooltip": "Cancella", + "@deleteDocumentTooltip": { + "description": "Tooltip shown for the delete button on details page" + }, + "downloadDocumentTooltip": "Scarica", + "@downloadDocumentTooltip": { + "description": "Tooltip shown for the download button on details page" + }, + "editDocumentTooltip": "Modifica", + "@editDocumentTooltip": { + "description": "Tooltip shown for the edit button on details page" + }, + "loadFullContent": "Carica intero contenuto", + "@loadFullContent": {}, + "noAppToDisplayPDFFilesFound": "Nessuna app trovata per mostrare i file PDF!", + "@noAppToDisplayPDFFilesFound": {}, + "openInSystemViewer": "Apri nel visualizzatore di sistema", + "@openInSystemViewer": {}, + "couldNotOpenFilePermissionDenied": "Impossibile aprire il file: Permesso negato.", + "@couldNotOpenFilePermissionDenied": {}, + "previewTooltip": "Anteprima", + "@previewTooltip": { + "description": "Tooltip shown for the preview button on details page" + }, + "shareTooltip": "Condividi", + "@shareTooltip": { + "description": "Tooltip shown for the share button on details page" + }, + "similarDocuments": "Documenti Simili", + "@similarDocuments": { + "description": "Label shown in the tabbar on details page" + }, + "content": "Contenuto", + "@content": { + "description": "Label shown in the tabbar on details page" + }, + "metaData": "Metadati", + "@metaData": { + "description": "Label shown in the tabbar on details page" + }, + "overview": "Panoramica", + "@overview": { + "description": "Label shown in the tabbar on details page" + }, + "documentType": "Tipo di documento", + "@documentType": {}, + "archivedPdf": "Archiviato (PDF)", + "@archivedPdf": { + "description": "Option to chose when downloading a document" + }, + "chooseFiletype": "Scegli tipo di file", + "@chooseFiletype": {}, + "original": "Originale", + "@original": { + "description": "Option to chose when downloading a document" + }, + "documentSuccessfullyDownloaded": "Documento scaricato con successo.", + "@documentSuccessfullyDownloaded": {}, + "suggestions": "Suggerimenti: ", + "@suggestions": {}, + "editDocument": "Modifica Documento", + "@editDocument": {}, + "advanced": "Avanzate", + "@advanced": {}, + "apply": "Applica", + "@apply": {}, + "extended": "Esteso", + "@extended": {}, + "titleAndContent": "Titolo E Contenuti", + "@titleAndContent": {}, + "title": "Titolo", + "@title": {}, + "reset": "Ripristina", + "@reset": {}, + "filterDocuments": "Filtra Documenti", + "@filterDocuments": { + "description": "Title of the document filter" + }, + "originalMD5Checksum": "Checksum MD5 Originale", + "@originalMD5Checksum": {}, + "mediaFilename": "Nome File Multimediale", + "@mediaFilename": {}, + "originalFileSize": "Dimensione File Originale", + "@originalFileSize": {}, + "originalMIMEType": "Tipo MIME Originale", + "@originalMIMEType": {}, + "modifiedAt": "Modificato il", + "@modifiedAt": {}, + "preview": "Anteprima", + "@preview": { + "description": "Title of the document preview page" + }, + "scanADocument": "Scansiona un documento", + "@scanADocument": {}, + "noDocumentsScannedYet": "Nessun documento ancora scansionato.", + "@noDocumentsScannedYet": {}, + "or": "o", + "@or": { + "description": "Used on the scanner page between both main actions when no scans have been captured." + }, + "deleteAllScans": "Elimina tutte le scansioni", + "@deleteAllScans": {}, + "uploadADocumentFromThisDevice": "Carica un documento da questo dispositivo", + "@uploadADocumentFromThisDevice": { + "description": "Button label on scanner page" + }, + "noMatchesFound": "Nessuna corrispondenza trovata.", + "@noMatchesFound": { + "description": "Displayed when no documents were found in the document search." + }, + "removeFromSearchHistory": "Rimuovere dalla cronologia di ricerca?", + "@removeFromSearchHistory": {}, + "results": "Risultati", + "@results": { + "description": "Label displayed above search results in document search." + }, + "searchDocuments": "Cerca documenti", + "@searchDocuments": {}, + "resetFilter": "Pulisci filtri", + "@resetFilter": {}, + "lastMonth": "Ultimo mese", + "@lastMonth": {}, + "last7Days": "Ultimi 7 giorni", + "@last7Days": {}, + "last3Months": "Ultimi 3 mesi", + "@last3Months": {}, + "lastYear": "Ultimo anno", + "@lastYear": {}, + "search": "Cerca", + "@search": {}, + "documentsSuccessfullyDeleted": "Documenti eliminati con successo.", + "@documentsSuccessfullyDeleted": {}, + "thereSeemsToBeNothingHere": "Sembra che non ci sia niente qui...", + "@thereSeemsToBeNothingHere": {}, + "oops": "Ops.", + "@oops": {}, + "newDocumentAvailable": "Nuovo documento disponibile!", + "@newDocumentAvailable": {}, + "orderBy": "Ordina per", + "@orderBy": {}, + "thisActionIsIrreversibleDoYouWishToProceedAnyway": "Questa azione è irreversibile. Vuoi procedere comunque?", + "@thisActionIsIrreversibleDoYouWishToProceedAnyway": {}, + "confirmDeletion": "Conferma l'eliminazione", + "@confirmDeletion": {}, + "areYouSureYouWantToDeleteTheFollowingDocuments": "{count, plural, one{Sei sicuro di voler eliminare il seguente documento?} other{Sei sicuro di voler eliminare i seguenti documenti?}}", + "@areYouSureYouWantToDeleteTheFollowingDocuments": { + "placeholders": { + "count": {} + } + }, + "countSelected": "{count} selezionati", + "@countSelected": { + "description": "Displayed in the appbar when at least one document is selected.", + "placeholders": { + "count": {} + } + }, + "storagePath": "Percorso Archiviazione", + "@storagePath": {}, + "prepareDocument": "Prepara documento", + "@prepareDocument": {}, + "tags": "Etichette", + "@tags": {}, + "documentSuccessfullyUpdated": "Documento aggiornato con successo.", + "@documentSuccessfullyUpdated": {}, + "fileName": "Nome file", + "@fileName": {}, + "synchronizeTitleAndFilename": "Sincronizza titolo e nome file", + "@synchronizeTitleAndFilename": {}, + "reload": "Ricarica", + "@reload": {}, + "documentSuccessfullyUploadedProcessing": "Documento caricato con successo, elaborazione...", + "@documentSuccessfullyUploadedProcessing": {}, + "deleteLabelWarningText": "Questa etichetta contiene riferimenti ad altri documenti. Eliminando questa etichetta, tutti i riferimenti verranno rimossi. Continuare?", + "@deleteLabelWarningText": {}, + "couldNotAcknowledgeTasks": "Impossibile riconoscere le attività.", + "@couldNotAcknowledgeTasks": {}, + "authenticationFailedPleaseTryAgain": "Errore di autenticazione. Riprova.", + "@authenticationFailedPleaseTryAgain": {}, + "anErrorOccurredWhileTryingToAutocompleteYourQuery": "Si è verificato un errore durante il tentativo di completare automaticamente la query.", + "@anErrorOccurredWhileTryingToAutocompleteYourQuery": {}, + "biometricAuthenticationFailed": "Autenticazione biometrica non riuscita.", + "@biometricAuthenticationFailed": {}, + "biometricAuthenticationNotSupported": "Autenticazione biometrica non supportata su questo dispositivo.", + "@biometricAuthenticationNotSupported": {}, + "couldNotBulkEditDocuments": "Impossibile modificare i documenti collettivamente.", + "@couldNotBulkEditDocuments": {}, + "couldNotCreateCorrespondent": "Impossibile creare il corrispondente, per favore riprova.", + "@couldNotCreateCorrespondent": {}, + "couldNotLoadCorrespondents": "Impossibile caricare i corrispondenti.", + "@couldNotLoadCorrespondents": {}, + "couldNotCreateSavedView": "Impossibile creare la vista salvata, riprova.", + "@couldNotCreateSavedView": {}, + "couldNotDeleteSavedView": "Impossibile eliminare la vista salvata, riprova", + "@couldNotDeleteSavedView": {}, + "youAreCurrentlyOffline": "Attualmente sei offline. Assicurati di essere connesso a internet.", + "@youAreCurrentlyOffline": {}, + "couldNotAssignArchiveSerialNumber": "Impossibile assegnare il numero seriale di archivio.", + "@couldNotAssignArchiveSerialNumber": {}, + "couldNotDeleteDocument": "Impossibile eliminare il documento, per favore riprova.", + "@couldNotDeleteDocument": {}, + "couldNotLoadDocuments": "Impossibile caricare il documento, per favore riprova.", + "@couldNotLoadDocuments": {}, + "couldNotLoadDocumentPreview": "Impossibile caricare l'anteprima del documento.", + "@couldNotLoadDocumentPreview": {}, + "couldNotCreateDocument": "Impossibile creare il documento, riprova.", + "@couldNotCreateDocument": {}, + "couldNotLoadDocumentTypes": "Impossibile caricare i tipi di documento, per favore riprova.", + "@couldNotLoadDocumentTypes": {}, + "couldNotUpdateDocument": "Impossibile aggiornare il documento, riprova.", + "@couldNotUpdateDocument": {}, + "couldNotUploadDocument": "Impossibile caricare il documento, per favore riprova.", + "@couldNotUploadDocument": {}, + "invalidCertificateOrMissingPassphrase": "Certificato non valido o passphrase mancante, riprova", + "@invalidCertificateOrMissingPassphrase": {}, + "couldNotLoadSavedViews": "Impossibile caricare le viste salvate.", + "@couldNotLoadSavedViews": {}, + "aClientCertificateWasExpectedButNotSent": "È previsto un certificato del client ma non è stato inviato. Si prega di fornire un certificato del client valido.", + "@aClientCertificateWasExpectedButNotSent": {}, + "userIsNotAuthenticated": "L'utente non è autenticato.", + "@userIsNotAuthenticated": {}, + "requestTimedOut": "La richiesta al server è scaduta.", + "@requestTimedOut": {}, + "anErrorOccurredRemovingTheScans": "Si è verificato un errore durante la rimozione delle scansioni.", + "@anErrorOccurredRemovingTheScans": {}, + "couldNotReachYourPaperlessServer": "Impossibile raggiungere il server Paperless, è in esecuzione?", + "@couldNotReachYourPaperlessServer": {}, + "couldNotLoadSimilarDocuments": "Impossibile caricare documenti simili.", + "@couldNotLoadSimilarDocuments": {}, + "couldNotCreateStoragePath": "Impossibile creare il percorso di archiviazione, per favore riprova.", + "@couldNotCreateStoragePath": {}, + "couldNotLoadStoragePaths": "Impossibile caricare i percorsi di archiviazione.", + "@couldNotLoadStoragePaths": {}, + "couldNotLoadSuggestions": "Impossibile caricare i suggerimenti.", + "@couldNotLoadSuggestions": {}, + "couldNotCreateTag": "Impossibile creare il tag, riprova.", + "@couldNotCreateTag": {}, + "couldNotLoadTags": "Impossibile caricare i tag.", + "@couldNotLoadTags": {}, + "anUnknownErrorOccurred": "Si è verificato un errore sconosciuto.", + "@anUnknownErrorOccurred": {}, + "fileFormatNotSupported": "Questo tipo di file non è supportato.", + "@fileFormatNotSupported": {}, + "report": "REPORT", + "@report": {}, + "absolute": "Assoluto", + "@absolute": {}, + "hintYouCanAlsoSpecifyRelativeValues": "Suggerimento: Oltre alle date specifiche, è possibile specificare un intervallo tra date.", + "@hintYouCanAlsoSpecifyRelativeValues": { + "description": "Displayed in the extended date range picker" + }, + "amount": "Quantità", + "@amount": {}, + "relative": "Relativo", + "@relative": {}, + "last": "Ultimo", + "@last": {}, + "timeUnit": "Unità di tempo", + "@timeUnit": {}, + "selectDateRange": "Seleziona intervallo date", + "@selectDateRange": {}, + "after": "Dopo", + "@after": {}, + "before": "Prima", + "@before": {}, + "days": "{count, plural, zero{giorni} one{giorno} other{giorni}}", + "@days": { + "placeholders": { + "count": {} + } + }, + "lastNDays": "{count, plural, zero{} one{Ieri} other{Ultimi {count} giorni}}", + "@lastNDays": { + "placeholders": { + "count": {} + } + }, + "lastNMonths": "{count, plural, zero{} one{Ultimo mese} other{Ultimi {count} mesi}}", + "@lastNMonths": { + "placeholders": { + "count": {} + } + }, + "lastNWeeks": "{count, plural, zero{} one{Ultima settimana} other{Ultime {count} settimane}}", + "@lastNWeeks": { + "placeholders": { + "count": {} + } + }, + "lastNYears": "{count, plural, zero{} one{Ultimo anno} other{Ultimi {count} anni}}", + "@lastNYears": { + "placeholders": { + "count": {} + } + }, + "months": "{count, plural, zero{} one{mese} other{mesi}}", + "@months": { + "placeholders": { + "count": {} + } + }, + "weeks": "{count, plural, zero{} one{settimana} other{settimane}}", + "@weeks": { + "placeholders": { + "count": {} + } + }, + "years": "{count, plural, zero{} one{anno} other{anni}}", + "@years": { + "placeholders": { + "count": {} + } + }, + "gotIt": "Capito!", + "@gotIt": {}, + "cancel": "Annulla", + "@cancel": {}, + "close": "Chiudi", + "@close": {}, + "create": "Crea documento", + "@create": {}, + "delete": "Cancella", + "@delete": {}, + "edit": "Modifica", + "@edit": {}, + "ok": "Conferma", + "@ok": {}, + "save": "Salva", + "@save": {}, + "select": "Seleziona", + "@select": {}, + "saveChanges": "Salva modifiche", + "@saveChanges": {}, + "upload": "Upload", + "@upload": {}, + "youreOffline": "Sei offline.", + "@youreOffline": {}, + "deleteDocument": "Elimina documento", + "@deleteDocument": { + "description": "Used as an action label on each inbox item" + }, + "removeDocumentFromInbox": "Documento rimosso dalla casella in arrivo.", + "@removeDocumentFromInbox": {}, + "areYouSureYouWantToMarkAllDocumentsAsSeen": "Sei sicuro di voler contrassegnare tutti i documenti come visti? Questo eseguirà un'operazione di modifica di massa rimuovendo tutti i tag di posta in arrivo dai documenti. Questa azione non è reversibile! Sei sicuro di voler continuare?", + "@areYouSureYouWantToMarkAllDocumentsAsSeen": {}, + "markAllAsSeen": "Contrassegna come Aperto?", + "@markAllAsSeen": {}, + "allSeen": "Tutto visto", + "@allSeen": {}, + "markAsSeen": "Contrassegna come Aperto", + "@markAsSeen": {}, + "refresh": "Aggiorna", + "@refresh": {}, + "youDoNotHaveUnseenDocuments": "Non hai documenti non visti.", + "@youDoNotHaveUnseenDocuments": {}, + "quickAction": "Azione rapida", + "@quickAction": {}, + "suggestionSuccessfullyApplied": "Suggerimento applicato con successo.", + "@suggestionSuccessfullyApplied": {}, + "today": "Oggi", + "@today": {}, + "undo": "Annullare", + "@undo": {}, + "nUnseen": "{count} non visti", + "@nUnseen": { + "placeholders": { + "count": {} + } + }, + "swipeLeftToMarkADocumentAsSeen": "Suggerimento: Scorri a sinistra per contrassegnare un documento come visto e rimuovere tutti i tag di posta in arrivo dal documento.", + "@swipeLeftToMarkADocumentAsSeen": {}, + "yesterday": "Ieri", + "@yesterday": {}, + "anyAssigned": "Qualunque assegnato", + "@anyAssigned": {}, + "noItemsFound": "Nessun elemento trovato!", + "@noItemsFound": {}, + "caseIrrelevant": "Maiuscolo/Minuscolo Irrilevante", + "@caseIrrelevant": {}, + "matchingAlgorithm": "Algoritmo Di Corrispondenza", + "@matchingAlgorithm": {}, + "match": "Corrispondenza", + "@match": {}, + "name": "Nome", + "@name": {}, + "notAssigned": "Non assegnato", + "@notAssigned": {}, + "addNewCorrespondent": "Aggiungi nuovo corrispondente", + "@addNewCorrespondent": {}, + "noCorrespondentsSetUp": "Sembra che non abbiate alcun corrispondente impostato.", + "@noCorrespondentsSetUp": {}, + "correspondents": "Corrispondenti", + "@correspondents": {}, + "addNewDocumentType": "Aggiungi nuovo tipo di documento", + "@addNewDocumentType": {}, + "noDocumentTypesSetUp": "Sembra che non abbiate impostato nessun tipo di documento.", + "@noDocumentTypesSetUp": {}, + "documentTypes": "Tipi Di Documento", + "@documentTypes": {}, + "addNewStoragePath": "Aggiungi un nuovo percorso di archiviazione", + "@addNewStoragePath": {}, + "noStoragePathsSetUp": "Sembra che non abbiate impostato alcun percorso di archiviazione.", + "@noStoragePathsSetUp": {}, + "storagePaths": "Percorsi Di Archiviazione", + "@storagePaths": {}, + "addNewTag": "Aggiungi nuova etichetta", + "@addNewTag": {}, + "noTagsSetUp": "Sembra che non abbiate impostato alcuna etichetta.", + "@noTagsSetUp": {}, + "linkedDocuments": "Documenti Collegati", + "@linkedDocuments": {}, + "advancedSettings": "Impostazioni avanzate", + "@advancedSettings": {}, + "passphrase": "Passphrase", + "@passphrase": {}, + "configureMutualTLSAuthentication": "Configura Autenticazione TLS Mutua", + "@configureMutualTLSAuthentication": {}, + "invalidCertificateFormat": "Formato del certificato non valido, è consentito solo .pfx", + "@invalidCertificateFormat": {}, + "clientcertificate": "Certificato Client", + "@clientcertificate": {}, + "selectFile": "Seleziona file...", + "@selectFile": {}, + "continueLabel": "Continua", + "@continueLabel": {}, + "incorrectOrMissingCertificatePassphrase": "Passphrase del certificato errata o mancante.", + "@incorrectOrMissingCertificatePassphrase": {}, + "connect": "Connessione", + "@connect": {}, + "password": "Password", + "@password": {}, + "passwordMustNotBeEmpty": "La password non deve essere vuota.", + "@passwordMustNotBeEmpty": {}, + "connectionTimedOut": "Connessione scaduta.", + "@connectionTimedOut": {}, + "loginPageReachabilityMissingClientCertificateText": "È previsto un certificato client ma non è stato inviato. Si prega di fornire un certificato client valido.", + "@loginPageReachabilityMissingClientCertificateText": {}, + "couldNotEstablishConnectionToTheServer": "Impossibile stabilire una connessione con il server.", + "@couldNotEstablishConnectionToTheServer": {}, + "connectionSuccessfulylEstablished": "Connessione stabilita con successo.", + "@connectionSuccessfulylEstablished": {}, + "hostCouldNotBeResolved": "L'host non può essere risolto. Controlla l'indirizzo del server e la tua connessione internet. ", + "@hostCouldNotBeResolved": {}, + "serverAddress": "Indirizzo Server", + "@serverAddress": {}, + "invalidAddress": "Indirizzo non valido.", + "@invalidAddress": {}, + "serverAddressMustIncludeAScheme": "L'indirizzo del server deve includere uno schema.", + "@serverAddressMustIncludeAScheme": {}, + "serverAddressMustNotBeEmpty": "L'indirizzo del server non deve essere vuoto.", + "@serverAddressMustNotBeEmpty": {}, + "signIn": "Accedi", + "@signIn": {}, + "loginPageSignInTitle": "Accedi", + "@loginPageSignInTitle": {}, + "signInToServer": "Accedi a {serverAddress}", + "@signInToServer": { + "placeholders": { + "serverAddress": {} + } + }, + "connectToPaperless": "Connettiti a Paperless", + "@connectToPaperless": {}, + "username": "Nome Utente", + "@username": {}, + "usernameMustNotBeEmpty": "Nome Utente non può essere vuoto.", + "@usernameMustNotBeEmpty": {}, + "documentContainsAllOfTheseWords": "Il documento contiene tutte queste parole", + "@documentContainsAllOfTheseWords": {}, + "all": "Tutti", + "@all": {}, + "documentContainsAnyOfTheseWords": "Il documento contiene una di queste parole", + "@documentContainsAnyOfTheseWords": {}, + "any": "Qualsiasi", + "@any": {}, + "learnMatchingAutomatically": "Impara automaticamente la corrispondenza", + "@learnMatchingAutomatically": {}, + "auto": "Auto", + "@auto": {}, + "documentContainsThisString": "Il documento contiene questa stringa", + "@documentContainsThisString": {}, + "exact": "Esatto", + "@exact": {}, + "documentContainsAWordSimilarToThisWord": "Il documento contiene una parola simile a questa", + "@documentContainsAWordSimilarToThisWord": {}, + "fuzzy": "Simile", + "@fuzzy": {}, + "documentMatchesThisRegularExpression": "Il documento corrisponde a questa espressione", + "@documentMatchesThisRegularExpression": {}, + "regularExpression": "Espressione", + "@regularExpression": {}, + "anInternetConnectionCouldNotBeEstablished": "Impossibile stabilire una connessione Internet.", + "@anInternetConnectionCouldNotBeEstablished": {}, + "done": "Fatto", + "@done": {}, + "next": "Successivo", + "@next": {}, + "couldNotAccessReceivedFile": "Impossibile accedere al file ricevuto. Prova ad aprire l'app prima di condividere.", + "@couldNotAccessReceivedFile": {}, + "newView": "Nuova visualizzazione", + "@newView": {}, + "createsASavedViewBasedOnTheCurrentFilterCriteria": "Crea una nuova visualizzazione in base ai criteri di filtro correnti.", + "@createsASavedViewBasedOnTheCurrentFilterCriteria": {}, + "createViewsToQuicklyFilterYourDocuments": "Crea viste per filtrare rapidamente i tuoi documenti.", + "@createViewsToQuicklyFilterYourDocuments": {}, + "nFiltersSet": "{count, plural, zero{{count} filtri impostati} one{{count} filtro impostato} other{{count} filtri impostati}}", + "@nFiltersSet": { + "placeholders": { + "count": {} + } + }, + "showInSidebar": "Visualizza nella barra laterale", + "@showInSidebar": {}, + "showOnDashboard": "Mostra nella dashboard", + "@showOnDashboard": {}, + "views": "Visualizzazioni", + "@views": {}, + "clearAll": "Cancella tutto", + "@clearAll": {}, + "scan": "Scansione", + "@scan": {}, + "previewScan": "Anteprima", + "@previewScan": {}, + "scrollToTop": "Scorri all'inizio", + "@scrollToTop": {}, + "paperlessServerVersion": "Versione server Paperless", + "@paperlessServerVersion": {}, + "darkTheme": "Tema Scuro", + "@darkTheme": {}, + "lightTheme": "Tema Chiaro", + "@lightTheme": {}, + "systemTheme": "Usa tema di sistema", + "@systemTheme": {}, + "appearance": "Aspetto", + "@appearance": {}, + "languageAndVisualAppearance": "Lingua e aspetto grafico", + "@languageAndVisualAppearance": {}, + "applicationSettings": "Applicazione", + "@applicationSettings": {}, + "colorSchemeHint": "Scegli tra uno schema di colori classico ispirato al verde tradizionale di Paperless o usa lo schema di colori dinamico basato sul tema del sistema.", + "@colorSchemeHint": {}, + "colorSchemeNotSupportedWarning": "Il tema dinamico è supportato solo per i dispositivi che eseguono Android 12 e versioni precedenti. Selezionando l'opzione 'Dinamica' potrebbe non avere alcun effetto in base all'implementazione del tuo sistema operativo.", + "@colorSchemeNotSupportedWarning": {}, + "colors": "Colori", + "@colors": {}, + "language": "Lingua", + "@language": {}, + "security": "Sicurezza", + "@security": {}, + "mangeFilesAndStorageSpace": "Gestisci file e spazio di archiviazione", + "@mangeFilesAndStorageSpace": {}, + "storage": "Memoria", + "@storage": {}, + "dark": "Scuro", + "@dark": {}, + "light": "Chiaro", + "@light": {}, + "system": "Sistema", + "@system": {}, + "ascending": "Crescente", + "@ascending": {}, + "descending": "Decrescente", + "@descending": {}, + "storagePathDay": "giorno", + "@storagePathDay": {}, + "storagePathMonth": "mese", + "@storagePathMonth": {}, + "storagePathYear": "anno", + "@storagePathYear": {}, + "color": "Colore", + "@color": {}, + "filterTags": "Filtra Tag...", + "@filterTags": {}, + "inboxTag": "Inbox-Tag", + "@inboxTag": {}, + "uploadInferValuesHint": "Se specifichi dei valori per questi campi, paperless non li compilerà in automatico. Se vuoi che questi campi siano compilati automaticamente dal server, lasciali in bianco.", + "@uploadInferValuesHint": {}, + "useTheConfiguredBiometricFactorToAuthenticate": "Usa il riconoscimento biometrico configurato per autenticarti e sbloccare i tuoi documenti.", + "@useTheConfiguredBiometricFactorToAuthenticate": {}, + "verifyYourIdentity": "Verifica la tua identità", + "@verifyYourIdentity": {}, + "verifyIdentity": "Verifica Identità", + "@verifyIdentity": {}, + "detailed": "Dettagliato", + "@detailed": {}, + "grid": "Griglia", + "@grid": {}, + "list": "Lista", + "@list": {}, + "remove": "Rimuovi", + "removeQueryFromSearchHistory": "Rimuovere dalla cronologia di ricerca?", + "dynamicColorScheme": "Dinamico", + "@dynamicColorScheme": {}, + "classicColorScheme": "Classico", + "@classicColorScheme": {}, + "notificationDownloadComplete": "Download completato", + "@notificationDownloadComplete": { + "description": "Notification title when a download has been completed." + }, + "notificationDownloadingDocument": "Download del documento", + "@notificationDownloadingDocument": { + "description": "Notification title shown when a document download is pending" + }, + "archiveSerialNumberUpdated": "Numero seriale di archivio aggiornato.", + "@archiveSerialNumberUpdated": { + "description": "Message shown when the ASN has been updated." + }, + "donateCoffee": "Offrimi un caffè", + "@donateCoffee": { + "description": "Label displayed in the app drawer" + }, + "thisFieldIsRequired": "Questo campo è obbligatorio!", + "@thisFieldIsRequired": { + "description": "Message shown below the form field when a required field has not been filled out." + }, + "confirm": "Conferma", + "confirmAction": "Conferma azione", + "@confirmAction": { + "description": "Typically used as a title to confirm a previously selected action" + }, + "areYouSureYouWantToContinue": "Sei sicuro di voler continuare?", + "bulkEditTagsAddMessage": "{count, plural, one{Questa operazione aggiungerà i tag {tags} al documento selezionato.} other{Questa operazione aggiungerà i tag {tags} ai {count} documenti selezionati.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{Questa operazione rimuoverà i tag {tags} dal documento selezionato.} other{Questa operazione rimuoverà i tag {tags} dai {count} documenti selezionati.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{Questa operazione aggiungerà i tag {addTags} e rimuoverà i tag {removeTags} dal documento selezionato.} other{Questa operazione aggiungerà i tag {addTags} e rimuoverà i tag {removeTags} dai {count} documenti selezionati.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{Questa operazione assegnerà i corrispondenti {correspondent} al documento selezionato.} other{Questa operazione assegnerà i corrispondenti {correspondent} ai {count} documenti selezionati.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{Questa operazione aggiungerà il tipo documento {docType} al documento selezionato.} other{Questa operazione aggiungerà il tipo documento {docType} ai {count} documenti selezionati.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{Questa operazione assegnerà il percorso di memorizzazione {path} al documento selezionato.} other{Questa operazione assegnerà il percorso di memorizzazione {path} ai {count} documenti selezionati.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{Questa operazione rimuoverà il correspondente dal documento selezionato.} other{Questa operazione rimuoverà il corrispondente dai {count} documenti selezionati.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{Questa operazione rimuoverà il tipo dal documento selezionato.} other{Questa operazione rimuoverà il tipo dai {count} documenti selezionati.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{Questa operazione rimuoverà il percorso di memorizzazione dal documento selezionato.} other{Questa operazione rimuoverà il percorso di memorizzazione dai {count} documenti selezionati.}}", + "anyTag": "Qualsiasi", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "Tutti", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + }, + "switchingAccountsPleaseWait": "Cambio account. Attendere prego...", + "@switchingAccountsPleaseWait": { + "description": "Message shown while switching accounts is in progress." + }, + "testConnection": "Test di connessione", + "@testConnection": { + "description": "Button label shown on login page. Allows user to test whether the server is reachable or not." + }, + "accounts": "Account", + "@accounts": { + "description": "Title of the account management dialog" + }, + "addAccount": "Aggiungi account", + "@addAccount": { + "description": "Label of add account action" + }, + "switchAccount": "Cambia", + "@switchAccount": { + "description": "Label for switch account action" + }, + "logout": "Esci", + "@logout": { + "description": "Generic Logout label" + }, + "switchAccountTitle": "Cambia account", + "@switchAccountTitle": { + "description": "Title of the dialog shown after adding an account, asking the user whether to switch to the newly added account or not." + }, + "switchToNewAccount": "Vuoi passare al nuovo account? Puoi tornare indietro in qualsiasi momento.", + "@switchToNewAccount": { + "description": "Content of the dialog shown after adding an account, asking the user whether to switch to the newly added account or not." + }, + "sourceCode": "Codice Sorgente", + "findTheSourceCodeOn": "Trova il codice sorgente attivo", + "@findTheSourceCodeOn": { + "description": "Text before link to Paperless Mobile GitHub" + }, + "rememberDecision": "Ricorda questa scelta", + "defaultDownloadFileType": "Download Tipo Di File Predefinito", + "@defaultDownloadFileType": { + "description": "Label indicating the default filetype to download (one of archived, original and always ask)" + }, + "defaultShareFileType": "Tipo Di File Condivisione Predefinito", + "@defaultShareFileType": { + "description": "Label indicating the default filetype to share (one of archived, original and always ask)" + }, + "alwaysAsk": "Chiedi sempre", + "@alwaysAsk": { + "description": "Option to choose when the app should always ask the user which filetype to use" + }, + "disableMatching": "Non taggare i documenti automaticamente", + "@disableMatching": { + "description": "One of the options for automatic tagging of documents" + }, + "none": "Nessuna", + "@none": { + "description": "One of available enum values of matching algorithm for tags" + }, + "logInToExistingAccount": "Accedi ad un account esistente", + "@logInToExistingAccount": { + "description": "Title shown on login page if at least one user is already known to the app." + }, + "print": "Stampa", + "@print": { + "description": "Tooltip for print button" + }, + "managePermissions": "Gestione autorizzazioni", + "@managePermissions": { + "description": "Button which leads user to manage permissions page" + }, + "errorRetrievingServerVersion": "Si è verificato un errore nella risoluzione della versione del server.", + "@errorRetrievingServerVersion": { + "description": "Message shown at the bottom of the settings page when the remote server version could not be resolved." + }, + "resolvingServerVersion": "Recupero versione server...", + "@resolvingServerVersion": { + "description": "Message shown while the app is loading the remote server version." + }, + "goToLogin": "Vai al login", + "@goToLogin": { + "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" + }, + "export": "Esporta", + "@export": { + "description": "Label for button that exports scanned images to pdf (before upload)" + }, + "invalidFilenameCharacter": "Carattere(i) non valido(i) trovato(i) nel nome del file: {characters}", + "@invalidFilenameCharacter": { + "description": "For validating filename in export dialogue" + }, + "exportScansToPdf": "Esporta scansioni in PDF", + "@exportScansToPdf": { + "description": "title of the alert dialog when exporting scans to pdf" + }, + "allScansWillBeMerged": "Tutte le scansioni saranno unite in un singolo file PDF.", + "behavior": "Comportamento", + "@behavior": { + "description": "Title of the settings concerning app beahvior" + }, + "theme": "Tema", + "@theme": { + "description": "Title of the theme mode setting" + }, + "clearCache": "Svuota cache", + "@clearCache": { + "description": "Title of the clear cache setting" + }, + "freeBytes": "Libero {byteString}", + "@freeBytes": { + "description": "Text shown for clear storage settings" + }, + "calculatingDots": "Calcolo...", + "@calculatingDots": { + "description": "Text shown when the byte size is still being calculated" + }, + "freedDiskSpace": "Liberato con successo {bytes} spazio su disco.", + "@freedDiskSpace": { + "description": "Message shown after clearing storage" + }, + "uploadScansAsPdf": "Carica scansioni in PDF", + "@uploadScansAsPdf": { + "description": "Title of the setting which toggles whether scans are always uploaded as pdf" + }, + "convertSinglePageScanToPdf": "Convertire sempre le scansioni di singola pagina in PDF prima di caricare", + "@convertSinglePageScanToPdf": { + "description": "description of the upload scans as pdf setting" + }, + "loginRequiredPermissionsHint": "L'utilizzo di Paperless Mobile richiede una serie minima di autorizzazioni utente a partire da paperless-ngx 1.14.0 e superiore. Pertanto, assicurati che l'utente abbia il permesso di visualizzare altri utenti (Utente → Visualizza) e le impostazioni (UISettings → Visualizza). Se non hai questi permessi, contatta un amministratore del tuo server paperless-ngx.", + "@loginRequiredPermissionsHint": { + "description": "Hint shown on the login page informing the user of the required permissions to use the app." + }, + "missingPermissions": "Non disponi dei permessi necessari per eseguire quest'azione.", + "@missingPermissions": { + "description": "Message shown in a snackbar when a user without the reequired permissions performs an action." + }, + "editView": "Modifica Visualizzazione", + "@editView": { + "description": "Title of the edit saved view page" + }, + "donate": "Fai una donazione", + "@donate": { + "description": "Label of the in-app donate button" + }, + "donationDialogContent": "Grazie per aver preso in considerazione di supportare questa app! In linea con le politiche di pagamento di Google e Apple, non è possibile visualizzare alcun link che porti a donazioni in-app. Non è consentito nemmeno il collegamento alla pagina della repository del progetto. Pertanto, sarà presente nella sezione 'Donazioni' nel README del progetto. Il tuo supporto è molto apprezzato e mantiene vivo lo sviluppo di questa app. Grazie!", + "@donationDialogContent": { + "description": "Text displayed in the donation dialog" + }, + "noDocumentsFound": "Nessun documento trovato.", + "@noDocumentsFound": { + "description": "Message shown when no documents were found." + }, + "couldNotDeleteCorrespondent": "Impossibile eliminare il corrispondente, per favore riprova.", + "@couldNotDeleteCorrespondent": { + "description": "Message shown in snackbar when a correspondent could not be deleted." + }, + "couldNotDeleteDocumentType": "Impossibile eliminare il tipo di documento, per favore riprova.", + "@couldNotDeleteDocumentType": { + "description": "Message shown when a document type could not be deleted" + }, + "couldNotDeleteTag": "Impossibile creare il tag, riprova.", + "@couldNotDeleteTag": { + "description": "Message shown when a tag could not be deleted" + }, + "couldNotDeleteStoragePath": "Impossibile eliminare il percorso di archiviazione, per favore riprova.", + "@couldNotDeleteStoragePath": { + "description": "Message shown when a storage path could not be deleted" + }, + "couldNotUpdateCorrespondent": "Impossibile aggiornare il corrispondente, per favore riprova.", + "@couldNotUpdateCorrespondent": { + "description": "Message shown when a correspondent could not be updated" + }, + "couldNotUpdateDocumentType": "Impossibile aggiornare il tipo di documento, per favore riprova.", + "@couldNotUpdateDocumentType": { + "description": "Message shown when a document type could not be updated" + }, + "couldNotUpdateTag": "Impossibile aggiornare il tag, per favore riprova.", + "@couldNotUpdateTag": { + "description": "Message shown when a tag could not be updated" + }, + "couldNotLoadServerInformation": "Impossibile caricare le informazioni del server.", + "@couldNotLoadServerInformation": { + "description": "Message shown when the server information could not be loaded" + }, + "couldNotLoadStatistics": "Impossibile caricare le statistiche del server.", + "@couldNotLoadStatistics": { + "description": "Message shown when the server statistics could not be loaded" + }, + "couldNotLoadUISettings": "Impossibile caricare impostazioni UI Interfaccia Utente.", + "@couldNotLoadUISettings": { + "description": "Message shown when the UI settings could not be loaded" + }, + "couldNotLoadTasks": "Impossibile caricare le attività.", + "@couldNotLoadTasks": { + "description": "Message shown when the tasks (e.g. document consumed) could not be loaded" + }, + "userNotFound": "Impossibile trovare l'utente.", + "@userNotFound": { + "description": "Message shown when the specified user (e.g. by id) could not be found" + }, + "couldNotUpdateSavedView": "Impossibile creare la vista salvata, riprova.", + "@couldNotUpdateSavedView": { + "description": "Message shown when a saved view could not be updated" + }, + "couldNotUpdateStoragePath": "Impossibile aggiornare il percorso di archiviazione, per favore riprova.", + "savedViewSuccessfullyUpdated": "Visualizzazione salvata aggiornata correttamente.", + "@savedViewSuccessfullyUpdated": { + "description": "Message shown when a saved view was successfully updated." + }, + "discardChanges": "Annulla modifiche?", + "@discardChanges": { + "description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes." + }, + "savedViewChangedDialogContent": "Le condizioni del filtro della vista attiva sono cambiate. Reimpostando il filtro, queste modifiche andranno perse. Desideri continuare?", + "@savedViewChangedDialogContent": { + "description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view." + }, + "createFromCurrentFilter": "Crea dal filtro corrente", + "@createFromCurrentFilter": { + "description": "Tooltip of the \"New saved view\" button" + }, + "home": "Home", + "@home": { + "description": "Label of the \"Home\" route" + }, + "welcomeUser": "Benvenuto, {name}!", + "@welcomeUser": { + "description": "Top message shown on the home page" + }, + "statistics": "Statistiche", + "documentsInInbox": "Documenti nella posta in arrivo", + "totalDocuments": "Totale documenti", + "totalCharacters": "Totale caratteri", + "showAll": "Mostra tutto", + "@showAll": { + "description": "Button label shown on a saved view preview to open this view in the documents page" + }, + "userAlreadyExists": "Utente già esistente.", + "@userAlreadyExists": { + "description": "Error message shown when the user tries to add an already existing account." + }, + "youDidNotSaveAnyViewsYet": "Non hai ancora salvato nessuna vista, creane una e verrà mostrata qui.", + "@youDidNotSaveAnyViewsYet": { + "description": "Message shown when there are no saved views yet." + }, + "tryAgain": "Riprova", + "discardFile": "Eliminare il file?", + "discard": "Elimina", + "backToLogin": "Torna al login", + "skipEditingReceivedFiles": "Salta la modifica dei file ricevuti", + "uploadWithoutPromptingUploadForm": "Carica sempre senza richiedere il modulo di caricamento quando condividi file con l'app.", + "authenticatingDots": "Accesso in corso...", + "@authenticatingDots": { + "description": "Message shown when the app is authenticating the user" + }, + "persistingUserInformation": "Salvataggio informazioni utente...", + "fetchingUserInformation": "Recupero informazioni utente...", + "@fetchingUserInformation": { + "description": "Message shown when the app loads user data from the server" + }, + "restoringSession": "Ripristino sessione...", + "@restoringSession": { + "description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in" + }, + "documentsAssigned": "{count, plural, zero{Nessun documento} one{1 documento} other{{count} documenti}}", + "@documentsAssigned": { + "description": "Text shown with a correspondent, document type etc. to indicate the number of documents this filter will maximally yield." + }, + "discardChangesWarning": "Sono state apportate modifiche non salvate. Continuando tutte le modifiche andranno perse. Eliminare queste modifiche?", + "@discardChangesWarning": { + "description": "Warning message shown when the user tries to close a route without saving the changes." + }, + "changelog": "Changelog", + "noLogsFoundOn": "Nessun log trovato il {date}.", + "logfileBottomReached": "Hai raggiunto la fine di questo file di log.", + "appLogs": "App logs {date}", + "saveLogsToFile": "Salva i log su file", + "copyToClipboard": "Copia negli appunti", + "couldNotLoadLogfileFrom": "Impossibile caricare il file di log da {date}.", + "loadingLogsFrom": "Caricamento log da {date}...", + "clearLogs": "Cancella log da {date}", + "showPdf": "Mostra PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Nascondi PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Varie", + "loggingOut": "Uscita in corso...", + "testingConnection": "Verifica connessione...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Versione {versionCode}", + "notes": "{count, plural, zero{Nota} one{Nota} other{Note}}", + "addNote": "Aggiungi nota", + "newerVersionAvailable": "Newer version available:" +} \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index 5a9c1b8..fab84f1 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -1026,5 +1026,6 @@ }, "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "addNote": "Add note", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index fb7fc7d..d03b374 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1026,5 +1026,6 @@ }, "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "addNote": "Add note", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb new file mode 100644 index 0000000..fab84f1 --- /dev/null +++ b/lib/l10n/intl_ro.arb @@ -0,0 +1,1031 @@ +{ + "developedBy": "Developed by {name}.", + "@developedBy": { + "placeholders": { + "name": {} + } + }, + "addAnotherAccount": "Add another account", + "@addAnotherAccount": {}, + "account": "Account", + "@account": {}, + "addCorrespondent": "New Correspondent", + "@addCorrespondent": { + "description": "Title when adding a new correspondent" + }, + "addDocumentType": "New Document Type", + "@addDocumentType": { + "description": "Title when adding a new document type" + }, + "addStoragePath": "New Storage Path", + "@addStoragePath": { + "description": "Title when adding a new storage path" + }, + "addTag": "New Tag", + "@addTag": { + "description": "Title when adding a new tag" + }, + "aboutThisApp": "About this app", + "@aboutThisApp": { + "description": "Label for about this app tile displayed in the drawer" + }, + "loggedInAs": "Logged in as {name}", + "@loggedInAs": { + "placeholders": { + "name": {} + } + }, + "disconnect": "Disconnect", + "@disconnect": { + "description": "Logout button label" + }, + "reportABug": "Report a Bug", + "@reportABug": {}, + "settings": "Settings", + "@settings": {}, + "authenticateOnAppStart": "Authenticate on app start", + "@authenticateOnAppStart": { + "description": "Description of the biometric authentication settings tile" + }, + "biometricAuthentication": "Biometric authentication", + "@biometricAuthentication": {}, + "authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate to enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}", + "@authenticateToToggleBiometricAuthentication": { + "placeholders": { + "mode": {} + } + }, + "documents": "Documents", + "@documents": {}, + "inbox": "Inbox", + "@inbox": {}, + "labels": "Labels", + "@labels": {}, + "scanner": "Scanner", + "@scanner": {}, + "startTyping": "Start typing...", + "@startTyping": {}, + "doYouReallyWantToDeleteThisView": "Do you really want to delete this view?", + "@doYouReallyWantToDeleteThisView": {}, + "deleteView": "Delete view {name}?", + "@deleteView": {}, + "addedAt": "Added at", + "@addedAt": {}, + "archiveSerialNumber": "Archive Serial Number", + "@archiveSerialNumber": {}, + "asn": "ASN", + "@asn": {}, + "correspondent": "Correspondent", + "@correspondent": {}, + "createdAt": "Created at", + "@createdAt": {}, + "documentSuccessfullyDeleted": "Document successfully deleted.", + "@documentSuccessfullyDeleted": {}, + "assignAsn": "Assign ASN", + "@assignAsn": {}, + "deleteDocumentTooltip": "Delete", + "@deleteDocumentTooltip": { + "description": "Tooltip shown for the delete button on details page" + }, + "downloadDocumentTooltip": "Download", + "@downloadDocumentTooltip": { + "description": "Tooltip shown for the download button on details page" + }, + "editDocumentTooltip": "Edit", + "@editDocumentTooltip": { + "description": "Tooltip shown for the edit button on details page" + }, + "loadFullContent": "Load full content", + "@loadFullContent": {}, + "noAppToDisplayPDFFilesFound": "No app to display PDF files found!", + "@noAppToDisplayPDFFilesFound": {}, + "openInSystemViewer": "Open in system viewer", + "@openInSystemViewer": {}, + "couldNotOpenFilePermissionDenied": "Could not open file: Permission denied.", + "@couldNotOpenFilePermissionDenied": {}, + "previewTooltip": "Preview", + "@previewTooltip": { + "description": "Tooltip shown for the preview button on details page" + }, + "shareTooltip": "Share", + "@shareTooltip": { + "description": "Tooltip shown for the share button on details page" + }, + "similarDocuments": "Similar Documents", + "@similarDocuments": { + "description": "Label shown in the tabbar on details page" + }, + "content": "Content", + "@content": { + "description": "Label shown in the tabbar on details page" + }, + "metaData": "Meta Data", + "@metaData": { + "description": "Label shown in the tabbar on details page" + }, + "overview": "Overview", + "@overview": { + "description": "Label shown in the tabbar on details page" + }, + "documentType": "Document Type", + "@documentType": {}, + "archivedPdf": "Archived (pdf)", + "@archivedPdf": { + "description": "Option to chose when downloading a document" + }, + "chooseFiletype": "Choose filetype", + "@chooseFiletype": {}, + "original": "Original", + "@original": { + "description": "Option to chose when downloading a document" + }, + "documentSuccessfullyDownloaded": "Document successfully downloaded.", + "@documentSuccessfullyDownloaded": {}, + "suggestions": "Suggestions: ", + "@suggestions": {}, + "editDocument": "Edit Document", + "@editDocument": {}, + "advanced": "Advanced", + "@advanced": {}, + "apply": "Apply", + "@apply": {}, + "extended": "Extended", + "@extended": {}, + "titleAndContent": "Title & Content", + "@titleAndContent": {}, + "title": "Title", + "@title": {}, + "reset": "Reset", + "@reset": {}, + "filterDocuments": "Filter Documents", + "@filterDocuments": { + "description": "Title of the document filter" + }, + "originalMD5Checksum": "Original MD5-Checksum", + "@originalMD5Checksum": {}, + "mediaFilename": "Media Filename", + "@mediaFilename": {}, + "originalFileSize": "Original File Size", + "@originalFileSize": {}, + "originalMIMEType": "Original MIME-Type", + "@originalMIMEType": {}, + "modifiedAt": "Modified at", + "@modifiedAt": {}, + "preview": "Preview", + "@preview": { + "description": "Title of the document preview page" + }, + "scanADocument": "Scan a document", + "@scanADocument": {}, + "noDocumentsScannedYet": "No documents scanned yet.", + "@noDocumentsScannedYet": {}, + "or": "or", + "@or": { + "description": "Used on the scanner page between both main actions when no scans have been captured." + }, + "deleteAllScans": "Delete all scans", + "@deleteAllScans": {}, + "uploadADocumentFromThisDevice": "Upload a document from this device", + "@uploadADocumentFromThisDevice": { + "description": "Button label on scanner page" + }, + "noMatchesFound": "No matches found.", + "@noMatchesFound": { + "description": "Displayed when no documents were found in the document search." + }, + "removeFromSearchHistory": "Remove from search history?", + "@removeFromSearchHistory": {}, + "results": "Results", + "@results": { + "description": "Label displayed above search results in document search." + }, + "searchDocuments": "Search documents", + "@searchDocuments": {}, + "resetFilter": "Reset filter", + "@resetFilter": {}, + "lastMonth": "Last Month", + "@lastMonth": {}, + "last7Days": "Last 7 Days", + "@last7Days": {}, + "last3Months": "Last 3 Months", + "@last3Months": {}, + "lastYear": "Last Year", + "@lastYear": {}, + "search": "Search", + "@search": {}, + "documentsSuccessfullyDeleted": "Documents successfully deleted.", + "@documentsSuccessfullyDeleted": {}, + "thereSeemsToBeNothingHere": "There seems to be nothing here...", + "@thereSeemsToBeNothingHere": {}, + "oops": "Oops.", + "@oops": {}, + "newDocumentAvailable": "New document available!", + "@newDocumentAvailable": {}, + "orderBy": "Order By", + "@orderBy": {}, + "thisActionIsIrreversibleDoYouWishToProceedAnyway": "This action is irreversible. Do you wish to proceed anyway?", + "@thisActionIsIrreversibleDoYouWishToProceedAnyway": {}, + "confirmDeletion": "Confirm deletion", + "@confirmDeletion": {}, + "areYouSureYouWantToDeleteTheFollowingDocuments": "{count, plural, one{Are you sure you want to delete the following document?} other{Are you sure you want to delete the following documents?}}", + "@areYouSureYouWantToDeleteTheFollowingDocuments": { + "placeholders": { + "count": {} + } + }, + "countSelected": "{count} selected", + "@countSelected": { + "description": "Displayed in the appbar when at least one document is selected.", + "placeholders": { + "count": {} + } + }, + "storagePath": "Storage Path", + "@storagePath": {}, + "prepareDocument": "Prepare document", + "@prepareDocument": {}, + "tags": "Tags", + "@tags": {}, + "documentSuccessfullyUpdated": "Document successfully updated.", + "@documentSuccessfullyUpdated": {}, + "fileName": "File Name", + "@fileName": {}, + "synchronizeTitleAndFilename": "Synchronize title and filename", + "@synchronizeTitleAndFilename": {}, + "reload": "Reload", + "@reload": {}, + "documentSuccessfullyUploadedProcessing": "Document successfully uploaded, processing...", + "@documentSuccessfullyUploadedProcessing": {}, + "deleteLabelWarningText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?", + "@deleteLabelWarningText": {}, + "couldNotAcknowledgeTasks": "Could not acknowledge tasks.", + "@couldNotAcknowledgeTasks": {}, + "authenticationFailedPleaseTryAgain": "Authentication failed, please try again.", + "@authenticationFailedPleaseTryAgain": {}, + "anErrorOccurredWhileTryingToAutocompleteYourQuery": "An error ocurred while trying to autocomplete your query.", + "@anErrorOccurredWhileTryingToAutocompleteYourQuery": {}, + "biometricAuthenticationFailed": "Biometric authentication failed.", + "@biometricAuthenticationFailed": {}, + "biometricAuthenticationNotSupported": "Biometric authentication not supported on this device.", + "@biometricAuthenticationNotSupported": {}, + "couldNotBulkEditDocuments": "Could not bulk edit documents.", + "@couldNotBulkEditDocuments": {}, + "couldNotCreateCorrespondent": "Could not create correspondent, please try again.", + "@couldNotCreateCorrespondent": {}, + "couldNotLoadCorrespondents": "Could not load correspondents.", + "@couldNotLoadCorrespondents": {}, + "couldNotCreateSavedView": "Could not create saved view, please try again.", + "@couldNotCreateSavedView": {}, + "couldNotDeleteSavedView": "Could not delete saved view, please try again", + "@couldNotDeleteSavedView": {}, + "youAreCurrentlyOffline": "You are currently offline. Please make sure you are connected to the internet.", + "@youAreCurrentlyOffline": {}, + "couldNotAssignArchiveSerialNumber": "Could not assign archive serial number.", + "@couldNotAssignArchiveSerialNumber": {}, + "couldNotDeleteDocument": "Could not delete document, please try again.", + "@couldNotDeleteDocument": {}, + "couldNotLoadDocuments": "Could not load documents, please try again.", + "@couldNotLoadDocuments": {}, + "couldNotLoadDocumentPreview": "Could not load document preview.", + "@couldNotLoadDocumentPreview": {}, + "couldNotCreateDocument": "Could not create document, please try again.", + "@couldNotCreateDocument": {}, + "couldNotLoadDocumentTypes": "Could not load document types, please try again.", + "@couldNotLoadDocumentTypes": {}, + "couldNotUpdateDocument": "Could not update document, please try again.", + "@couldNotUpdateDocument": {}, + "couldNotUploadDocument": "Could not upload document, please try again.", + "@couldNotUploadDocument": {}, + "invalidCertificateOrMissingPassphrase": "Invalid certificate or missing passphrase, please try again", + "@invalidCertificateOrMissingPassphrase": {}, + "couldNotLoadSavedViews": "Could not load saved views.", + "@couldNotLoadSavedViews": {}, + "aClientCertificateWasExpectedButNotSent": "A client certificate was expected but not sent. Please provide a valid client certificate.", + "@aClientCertificateWasExpectedButNotSent": {}, + "userIsNotAuthenticated": "User is not authenticated.", + "@userIsNotAuthenticated": {}, + "requestTimedOut": "The request to the server timed out.", + "@requestTimedOut": {}, + "anErrorOccurredRemovingTheScans": "An error occurred removing the scans.", + "@anErrorOccurredRemovingTheScans": {}, + "couldNotReachYourPaperlessServer": "Could not reach your Paperless server, is it up and running?", + "@couldNotReachYourPaperlessServer": {}, + "couldNotLoadSimilarDocuments": "Could not load similar documents.", + "@couldNotLoadSimilarDocuments": {}, + "couldNotCreateStoragePath": "Could not create storage path, please try again.", + "@couldNotCreateStoragePath": {}, + "couldNotLoadStoragePaths": "Could not load storage paths.", + "@couldNotLoadStoragePaths": {}, + "couldNotLoadSuggestions": "Could not load suggestions.", + "@couldNotLoadSuggestions": {}, + "couldNotCreateTag": "Could not create tag, please try again.", + "@couldNotCreateTag": {}, + "couldNotLoadTags": "Could not load tags.", + "@couldNotLoadTags": {}, + "anUnknownErrorOccurred": "An unknown error occurred.", + "@anUnknownErrorOccurred": {}, + "fileFormatNotSupported": "This file format is not supported.", + "@fileFormatNotSupported": {}, + "report": "REPORT", + "@report": {}, + "absolute": "Absolute", + "@absolute": {}, + "hintYouCanAlsoSpecifyRelativeValues": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.", + "@hintYouCanAlsoSpecifyRelativeValues": { + "description": "Displayed in the extended date range picker" + }, + "amount": "Amount", + "@amount": {}, + "relative": "Relative", + "@relative": {}, + "last": "Last", + "@last": {}, + "timeUnit": "Time unit", + "@timeUnit": {}, + "selectDateRange": "Select date range", + "@selectDateRange": {}, + "after": "After", + "@after": {}, + "before": "Before", + "@before": {}, + "days": "{count, plural, zero{days} one{day} other{days}}", + "@days": { + "placeholders": { + "count": {} + } + }, + "lastNDays": "{count, plural, zero{} one{Yesterday} other{Last {count} days}}", + "@lastNDays": { + "placeholders": { + "count": {} + } + }, + "lastNMonths": "{count, plural, zero{} one{Last month} other{Last {count} months}}", + "@lastNMonths": { + "placeholders": { + "count": {} + } + }, + "lastNWeeks": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}", + "@lastNWeeks": { + "placeholders": { + "count": {} + } + }, + "lastNYears": "{count, plural, zero{} one{Last year} other{Last {count} years}}", + "@lastNYears": { + "placeholders": { + "count": {} + } + }, + "months": "{count, plural, zero{} one{month} other{months}}", + "@months": { + "placeholders": { + "count": {} + } + }, + "weeks": "{count, plural, zero{} one{week} other{weeks}}", + "@weeks": { + "placeholders": { + "count": {} + } + }, + "years": "{count, plural, zero{} one{year} other{years}}", + "@years": { + "placeholders": { + "count": {} + } + }, + "gotIt": "Got it!", + "@gotIt": {}, + "cancel": "Cancel", + "@cancel": {}, + "close": "Close", + "@close": {}, + "create": "Create", + "@create": {}, + "delete": "Delete", + "@delete": {}, + "edit": "Edit", + "@edit": {}, + "ok": "Ok", + "@ok": {}, + "save": "Save", + "@save": {}, + "select": "Select", + "@select": {}, + "saveChanges": "Save changes", + "@saveChanges": {}, + "upload": "Upload", + "@upload": {}, + "youreOffline": "You're offline.", + "@youreOffline": {}, + "deleteDocument": "Delete document", + "@deleteDocument": { + "description": "Used as an action label on each inbox item" + }, + "removeDocumentFromInbox": "Document removed from inbox.", + "@removeDocumentFromInbox": {}, + "areYouSureYouWantToMarkAllDocumentsAsSeen": "Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents. This action is not reversible! Are you sure you want to continue?", + "@areYouSureYouWantToMarkAllDocumentsAsSeen": {}, + "markAllAsSeen": "Mark all as seen?", + "@markAllAsSeen": {}, + "allSeen": "All seen", + "@allSeen": {}, + "markAsSeen": "Mark as seen", + "@markAsSeen": {}, + "refresh": "Refresh", + "@refresh": {}, + "youDoNotHaveUnseenDocuments": "You do not have unseen documents.", + "@youDoNotHaveUnseenDocuments": {}, + "quickAction": "Quick Action", + "@quickAction": {}, + "suggestionSuccessfullyApplied": "Suggestion successfully applied.", + "@suggestionSuccessfullyApplied": {}, + "today": "Today", + "@today": {}, + "undo": "Undo", + "@undo": {}, + "nUnseen": "{count} unseen", + "@nUnseen": { + "placeholders": { + "count": {} + } + }, + "swipeLeftToMarkADocumentAsSeen": "Hint: Swipe left to mark a document as seen and remove all inbox tags from the document.", + "@swipeLeftToMarkADocumentAsSeen": {}, + "yesterday": "Yesterday", + "@yesterday": {}, + "anyAssigned": "Any assigned", + "@anyAssigned": {}, + "noItemsFound": "No items found!", + "@noItemsFound": {}, + "caseIrrelevant": "Case Irrelevant", + "@caseIrrelevant": {}, + "matchingAlgorithm": "Matching Algorithm", + "@matchingAlgorithm": {}, + "match": "Match", + "@match": {}, + "name": "Name", + "@name": {}, + "notAssigned": "Not assigned", + "@notAssigned": {}, + "addNewCorrespondent": "Add new correspondent", + "@addNewCorrespondent": {}, + "noCorrespondentsSetUp": "You don't seem to have any correspondents set up.", + "@noCorrespondentsSetUp": {}, + "correspondents": "Correspondents", + "@correspondents": {}, + "addNewDocumentType": "Add new document type", + "@addNewDocumentType": {}, + "noDocumentTypesSetUp": "You don't seem to have any document types set up.", + "@noDocumentTypesSetUp": {}, + "documentTypes": "Document Types", + "@documentTypes": {}, + "addNewStoragePath": "Add new storage path", + "@addNewStoragePath": {}, + "noStoragePathsSetUp": "You don't seem to have any storage paths set up.", + "@noStoragePathsSetUp": {}, + "storagePaths": "Storage Paths", + "@storagePaths": {}, + "addNewTag": "Add new tag", + "@addNewTag": {}, + "noTagsSetUp": "You don't seem to have any tags set up.", + "@noTagsSetUp": {}, + "linkedDocuments": "Linked Documents", + "@linkedDocuments": {}, + "advancedSettings": "Advanced Settings", + "@advancedSettings": {}, + "passphrase": "Passphrase", + "@passphrase": {}, + "configureMutualTLSAuthentication": "Configure Mutual TLS Authentication", + "@configureMutualTLSAuthentication": {}, + "invalidCertificateFormat": "Invalid certificate format, only .pfx is allowed", + "@invalidCertificateFormat": {}, + "clientcertificate": "Client Certificate", + "@clientcertificate": {}, + "selectFile": "Select file...", + "@selectFile": {}, + "continueLabel": "Continue", + "@continueLabel": {}, + "incorrectOrMissingCertificatePassphrase": "Incorrect or missing certificate passphrase.", + "@incorrectOrMissingCertificatePassphrase": {}, + "connect": "Connect", + "@connect": {}, + "password": "Password", + "@password": {}, + "passwordMustNotBeEmpty": "Password must not be empty.", + "@passwordMustNotBeEmpty": {}, + "connectionTimedOut": "Connection timed out.", + "@connectionTimedOut": {}, + "loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.", + "@loginPageReachabilityMissingClientCertificateText": {}, + "couldNotEstablishConnectionToTheServer": "Could not establish a connection to the server.", + "@couldNotEstablishConnectionToTheServer": {}, + "connectionSuccessfulylEstablished": "Connection successfully established.", + "@connectionSuccessfulylEstablished": {}, + "hostCouldNotBeResolved": "Host could not be resolved. Please check the server address and your internet connection. ", + "@hostCouldNotBeResolved": {}, + "serverAddress": "Server Address", + "@serverAddress": {}, + "invalidAddress": "Invalid address.", + "@invalidAddress": {}, + "serverAddressMustIncludeAScheme": "Server address must include a scheme.", + "@serverAddressMustIncludeAScheme": {}, + "serverAddressMustNotBeEmpty": "Server address must not be empty.", + "@serverAddressMustNotBeEmpty": {}, + "signIn": "Sign In", + "@signIn": {}, + "loginPageSignInTitle": "Sign In", + "@loginPageSignInTitle": {}, + "signInToServer": "Sign in to {serverAddress}", + "@signInToServer": { + "placeholders": { + "serverAddress": {} + } + }, + "connectToPaperless": "Connect to Paperless", + "@connectToPaperless": {}, + "username": "Username", + "@username": {}, + "usernameMustNotBeEmpty": "Username must not be empty.", + "@usernameMustNotBeEmpty": {}, + "documentContainsAllOfTheseWords": "Document contains all of these words", + "@documentContainsAllOfTheseWords": {}, + "all": "All", + "@all": {}, + "documentContainsAnyOfTheseWords": "Document contains any of these words", + "@documentContainsAnyOfTheseWords": {}, + "any": "Any", + "@any": {}, + "learnMatchingAutomatically": "Learn matching automatically", + "@learnMatchingAutomatically": {}, + "auto": "Auto", + "@auto": {}, + "documentContainsThisString": "Document contains this string", + "@documentContainsThisString": {}, + "exact": "Exact", + "@exact": {}, + "documentContainsAWordSimilarToThisWord": "Document contains a word similar to this word", + "@documentContainsAWordSimilarToThisWord": {}, + "fuzzy": "Fuzzy", + "@fuzzy": {}, + "documentMatchesThisRegularExpression": "Document matches this regular expression", + "@documentMatchesThisRegularExpression": {}, + "regularExpression": "Regular Expression", + "@regularExpression": {}, + "anInternetConnectionCouldNotBeEstablished": "An internet connection could not be established.", + "@anInternetConnectionCouldNotBeEstablished": {}, + "done": "Done", + "@done": {}, + "next": "Next", + "@next": {}, + "couldNotAccessReceivedFile": "Could not access the received file. Please try to open the app before sharing.", + "@couldNotAccessReceivedFile": {}, + "newView": "New View", + "@newView": {}, + "createsASavedViewBasedOnTheCurrentFilterCriteria": "Creates a new view based on the current filter criteria.", + "@createsASavedViewBasedOnTheCurrentFilterCriteria": {}, + "createViewsToQuicklyFilterYourDocuments": "Create views to quickly filter your documents.", + "@createViewsToQuicklyFilterYourDocuments": {}, + "nFiltersSet": "{count, plural, zero{{count} filters set} one{{count} filter set} other{{count} filters set}}", + "@nFiltersSet": { + "placeholders": { + "count": {} + } + }, + "showInSidebar": "Show in sidebar", + "@showInSidebar": {}, + "showOnDashboard": "Show on dashboard", + "@showOnDashboard": {}, + "views": "Views", + "@views": {}, + "clearAll": "Clear all", + "@clearAll": {}, + "scan": "Scan", + "@scan": {}, + "previewScan": "Preview", + "@previewScan": {}, + "scrollToTop": "Scroll to top", + "@scrollToTop": {}, + "paperlessServerVersion": "Paperless server version", + "@paperlessServerVersion": {}, + "darkTheme": "Dark Theme", + "@darkTheme": {}, + "lightTheme": "Light Theme", + "@lightTheme": {}, + "systemTheme": "Use system theme", + "@systemTheme": {}, + "appearance": "Appearance", + "@appearance": {}, + "languageAndVisualAppearance": "Language and visual appearance", + "@languageAndVisualAppearance": {}, + "applicationSettings": "Application", + "@applicationSettings": {}, + "colorSchemeHint": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", + "@colorSchemeHint": {}, + "colorSchemeNotSupportedWarning": "Dynamic theming is only supported for devices running Android 12 and above. Selecting the 'Dynamic' option might not have any effect depending on your OS implementation.", + "@colorSchemeNotSupportedWarning": {}, + "colors": "Colors", + "@colors": {}, + "language": "Language", + "@language": {}, + "security": "Security", + "@security": {}, + "mangeFilesAndStorageSpace": "Manage files and storage space", + "@mangeFilesAndStorageSpace": {}, + "storage": "Storage", + "@storage": {}, + "dark": "Dark", + "@dark": {}, + "light": "Light", + "@light": {}, + "system": "System", + "@system": {}, + "ascending": "Ascending", + "@ascending": {}, + "descending": "Descending", + "@descending": {}, + "storagePathDay": "day", + "@storagePathDay": {}, + "storagePathMonth": "month", + "@storagePathMonth": {}, + "storagePathYear": "year", + "@storagePathYear": {}, + "color": "Color", + "@color": {}, + "filterTags": "Filter tags...", + "@filterTags": {}, + "inboxTag": "Inbox-Tag", + "@inboxTag": {}, + "uploadInferValuesHint": "If you specify values for these fields, your paperless instance will not automatically derive a value. If you want these values to be automatically populated by your server, leave the fields blank.", + "@uploadInferValuesHint": {}, + "useTheConfiguredBiometricFactorToAuthenticate": "Use the configured biometric factor to authenticate and unlock your documents.", + "@useTheConfiguredBiometricFactorToAuthenticate": {}, + "verifyYourIdentity": "Verify your identity", + "@verifyYourIdentity": {}, + "verifyIdentity": "Verify Identity", + "@verifyIdentity": {}, + "detailed": "Detailed", + "@detailed": {}, + "grid": "Grid", + "@grid": {}, + "list": "List", + "@list": {}, + "remove": "Remove", + "removeQueryFromSearchHistory": "Remove query from search history?", + "dynamicColorScheme": "Dynamic", + "@dynamicColorScheme": {}, + "classicColorScheme": "Classic", + "@classicColorScheme": {}, + "notificationDownloadComplete": "Download complete", + "@notificationDownloadComplete": { + "description": "Notification title when a download has been completed." + }, + "notificationDownloadingDocument": "Downloading document", + "@notificationDownloadingDocument": { + "description": "Notification title shown when a document download is pending" + }, + "archiveSerialNumberUpdated": "Archive Serial Number updated.", + "@archiveSerialNumberUpdated": { + "description": "Message shown when the ASN has been updated." + }, + "donateCoffee": "Buy me a coffee", + "@donateCoffee": { + "description": "Label displayed in the app drawer" + }, + "thisFieldIsRequired": "This field is required!", + "@thisFieldIsRequired": { + "description": "Message shown below the form field when a required field has not been filled out." + }, + "confirm": "Confirm", + "confirmAction": "Confirm action", + "@confirmAction": { + "description": "Typically used as a title to confirm a previously selected action" + }, + "areYouSureYouWantToContinue": "Are you sure you want to continue?", + "bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}", + "@bulkEditTagsAddMessage": { + "description": "Message of the confirmation dialog when bulk adding tags." + }, + "bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}", + "@bulkEditTagsRemoveMessage": { + "description": "Message of the confirmation dialog when bulk removing tags." + }, + "bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}", + "@bulkEditTagsModifyMessage": { + "description": "Message of the confirmation dialog when both adding and removing tags." + }, + "bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}", + "bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}", + "bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}", + "bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent from the selected document.} other{This operation will remove the correspondent from {count} selected documents.}}", + "bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type from the selected document.} other{This operation will remove the document type from {count} selected documents.}}", + "bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path from the selected document.} other{This operation will remove the storage path from {count} selected documents.}}", + "anyTag": "Any", + "@anyTag": { + "description": "Label shown when any tag should be filtered" + }, + "allTags": "All", + "@allTags": { + "description": "Label shown when a document has to be assigned to all selected tags" + }, + "switchingAccountsPleaseWait": "Switching accounts. Please wait...", + "@switchingAccountsPleaseWait": { + "description": "Message shown while switching accounts is in progress." + }, + "testConnection": "Test connection", + "@testConnection": { + "description": "Button label shown on login page. Allows user to test whether the server is reachable or not." + }, + "accounts": "Accounts", + "@accounts": { + "description": "Title of the account management dialog" + }, + "addAccount": "Add account", + "@addAccount": { + "description": "Label of add account action" + }, + "switchAccount": "Switch", + "@switchAccount": { + "description": "Label for switch account action" + }, + "logout": "Logout", + "@logout": { + "description": "Generic Logout label" + }, + "switchAccountTitle": "Switch account", + "@switchAccountTitle": { + "description": "Title of the dialog shown after adding an account, asking the user whether to switch to the newly added account or not." + }, + "switchToNewAccount": "Do you want to switch to the new account? You can switch back at any time.", + "@switchToNewAccount": { + "description": "Content of the dialog shown after adding an account, asking the user whether to switch to the newly added account or not." + }, + "sourceCode": "Source Code", + "findTheSourceCodeOn": "Find the source code on", + "@findTheSourceCodeOn": { + "description": "Text before link to Paperless Mobile GitHub" + }, + "rememberDecision": "Remember my decision", + "defaultDownloadFileType": "Default Download File Type", + "@defaultDownloadFileType": { + "description": "Label indicating the default filetype to download (one of archived, original and always ask)" + }, + "defaultShareFileType": "Default Share File Type", + "@defaultShareFileType": { + "description": "Label indicating the default filetype to share (one of archived, original and always ask)" + }, + "alwaysAsk": "Always ask", + "@alwaysAsk": { + "description": "Option to choose when the app should always ask the user which filetype to use" + }, + "disableMatching": "Do not tag documents automatically", + "@disableMatching": { + "description": "One of the options for automatic tagging of documents" + }, + "none": "None", + "@none": { + "description": "One of available enum values of matching algorithm for tags" + }, + "logInToExistingAccount": "Log in to existing account", + "@logInToExistingAccount": { + "description": "Title shown on login page if at least one user is already known to the app." + }, + "print": "Print", + "@print": { + "description": "Tooltip for print button" + }, + "managePermissions": "Manage permissions", + "@managePermissions": { + "description": "Button which leads user to manage permissions page" + }, + "errorRetrievingServerVersion": "An error occurred trying to resolve the server version.", + "@errorRetrievingServerVersion": { + "description": "Message shown at the bottom of the settings page when the remote server version could not be resolved." + }, + "resolvingServerVersion": "Resolving server version...", + "@resolvingServerVersion": { + "description": "Message shown while the app is loading the remote server version." + }, + "goToLogin": "Go to login", + "@goToLogin": { + "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" + }, + "export": "Export", + "@export": { + "description": "Label for button that exports scanned images to pdf (before upload)" + }, + "invalidFilenameCharacter": "Invalid character(s) found in filename: {characters}", + "@invalidFilenameCharacter": { + "description": "For validating filename in export dialogue" + }, + "exportScansToPdf": "Export scans to PDF", + "@exportScansToPdf": { + "description": "title of the alert dialog when exporting scans to pdf" + }, + "allScansWillBeMerged": "All scans will be merged into a single PDF file.", + "behavior": "Behavior", + "@behavior": { + "description": "Title of the settings concerning app beahvior" + }, + "theme": "Theme", + "@theme": { + "description": "Title of the theme mode setting" + }, + "clearCache": "Clear cache", + "@clearCache": { + "description": "Title of the clear cache setting" + }, + "freeBytes": "Free {byteString}", + "@freeBytes": { + "description": "Text shown for clear storage settings" + }, + "calculatingDots": "Calculating...", + "@calculatingDots": { + "description": "Text shown when the byte size is still being calculated" + }, + "freedDiskSpace": "Successfully freed {bytes} of disk space.", + "@freedDiskSpace": { + "description": "Message shown after clearing storage" + }, + "uploadScansAsPdf": "Upload scans as PDF", + "@uploadScansAsPdf": { + "description": "Title of the setting which toggles whether scans are always uploaded as pdf" + }, + "convertSinglePageScanToPdf": "Always convert single page scans to PDF before uploading", + "@convertSinglePageScanToPdf": { + "description": "description of the upload scans as pdf setting" + }, + "loginRequiredPermissionsHint": "Using Paperless Mobile requires a minimum set of user permissions since paperless-ngx 1.14.0 and higher. Therefore, please make sure that the user to be logged in has the permission to view other users (User → View) and the settings (UISettings → View). If you do not have these permissions, please contact an administrator of your paperless-ngx server.", + "@loginRequiredPermissionsHint": { + "description": "Hint shown on the login page informing the user of the required permissions to use the app." + }, + "missingPermissions": "You do not have the necessary permissions to perform this action.", + "@missingPermissions": { + "description": "Message shown in a snackbar when a user without the reequired permissions performs an action." + }, + "editView": "Edit View", + "@editView": { + "description": "Title of the edit saved view page" + }, + "donate": "Donate", + "@donate": { + "description": "Label of the in-app donate button" + }, + "donationDialogContent": "Thank you for considering to support this app! Due to both Google's and Apple's Payment Policies, no links leading to donations may be displayed in-app. Not even linking to the project's repository page appears to be allowed in this context. Therefore, maybe have a look at the 'Donations' section in the project's README. Your support is much appreciated and keeps the development of this app alive. Thanks!", + "@donationDialogContent": { + "description": "Text displayed in the donation dialog" + }, + "noDocumentsFound": "No documents found.", + "@noDocumentsFound": { + "description": "Message shown when no documents were found." + }, + "couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.", + "@couldNotDeleteCorrespondent": { + "description": "Message shown in snackbar when a correspondent could not be deleted." + }, + "couldNotDeleteDocumentType": "Could not delete document type, please try again.", + "@couldNotDeleteDocumentType": { + "description": "Message shown when a document type could not be deleted" + }, + "couldNotDeleteTag": "Could not delete tag, please try again.", + "@couldNotDeleteTag": { + "description": "Message shown when a tag could not be deleted" + }, + "couldNotDeleteStoragePath": "Could not delete storage path, please try again.", + "@couldNotDeleteStoragePath": { + "description": "Message shown when a storage path could not be deleted" + }, + "couldNotUpdateCorrespondent": "Could not update correspondent, please try again.", + "@couldNotUpdateCorrespondent": { + "description": "Message shown when a correspondent could not be updated" + }, + "couldNotUpdateDocumentType": "Could not update document type, please try again.", + "@couldNotUpdateDocumentType": { + "description": "Message shown when a document type could not be updated" + }, + "couldNotUpdateTag": "Could not update tag, please try again.", + "@couldNotUpdateTag": { + "description": "Message shown when a tag could not be updated" + }, + "couldNotLoadServerInformation": "Could not load server information.", + "@couldNotLoadServerInformation": { + "description": "Message shown when the server information could not be loaded" + }, + "couldNotLoadStatistics": "Could not load server statistics.", + "@couldNotLoadStatistics": { + "description": "Message shown when the server statistics could not be loaded" + }, + "couldNotLoadUISettings": "Could not load UI settings.", + "@couldNotLoadUISettings": { + "description": "Message shown when the UI settings could not be loaded" + }, + "couldNotLoadTasks": "Could not load tasks.", + "@couldNotLoadTasks": { + "description": "Message shown when the tasks (e.g. document consumed) could not be loaded" + }, + "userNotFound": "User could not be found.", + "@userNotFound": { + "description": "Message shown when the specified user (e.g. by id) could not be found" + }, + "couldNotUpdateSavedView": "Could not update saved view, please try again.", + "@couldNotUpdateSavedView": { + "description": "Message shown when a saved view could not be updated" + }, + "couldNotUpdateStoragePath": "Could not update storage path, please try again.", + "savedViewSuccessfullyUpdated": "Saved view successfully updated.", + "@savedViewSuccessfullyUpdated": { + "description": "Message shown when a saved view was successfully updated." + }, + "discardChanges": "Discard changes?", + "@discardChanges": { + "description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes." + }, + "savedViewChangedDialogContent": "The filter conditions of the active view have changed. By resetting the filter, these changes will be lost. Do you still wish to continue?", + "@savedViewChangedDialogContent": { + "description": "Content of the alert dialog shown when all of the following applies:\r\n* User has saved view selected\r\n* User has performed changes to the current document filter\r\n* User now tries to reset this filter without having saved the changes to the view." + }, + "createFromCurrentFilter": "Create from current filter", + "@createFromCurrentFilter": { + "description": "Tooltip of the \"New saved view\" button" + }, + "home": "Home", + "@home": { + "description": "Label of the \"Home\" route" + }, + "welcomeUser": "Welcome, {name}!", + "@welcomeUser": { + "description": "Top message shown on the home page" + }, + "statistics": "Statistics", + "documentsInInbox": "Documents in inbox", + "totalDocuments": "Total documents", + "totalCharacters": "Total characters", + "showAll": "Show all", + "@showAll": { + "description": "Button label shown on a saved view preview to open this view in the documents page" + }, + "userAlreadyExists": "This user already exists.", + "@userAlreadyExists": { + "description": "Error message shown when the user tries to add an already existing account." + }, + "youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.", + "@youDidNotSaveAnyViewsYet": { + "description": "Message shown when there are no saved views yet." + }, + "tryAgain": "Try again", + "discardFile": "Discard file?", + "discard": "Discard", + "backToLogin": "Back to login", + "skipEditingReceivedFiles": "Skip editing received files", + "uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.", + "authenticatingDots": "Authenticating...", + "@authenticatingDots": { + "description": "Message shown when the app is authenticating the user" + }, + "persistingUserInformation": "Persisting user information...", + "fetchingUserInformation": "Fetching user information...", + "@fetchingUserInformation": { + "description": "Message shown when the app loads user data from the server" + }, + "restoringSession": "Restoring session...", + "@restoringSession": { + "description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in" + }, + "documentsAssigned": "{count, plural, zero{No documents} one{1 document} other{{count} documents}}", + "@documentsAssigned": { + "description": "Text shown with a correspondent, document type etc. to indicate the number of documents this filter will maximally yield." + }, + "discardChangesWarning": "You have unsaved changes. By continuing, all changes will be lost. Do you want to discard these changes?", + "@discardChangesWarning": { + "description": "Warning message shown when the user tries to close a route without saving the changes." + }, + "changelog": "Changelog", + "noLogsFoundOn": "No logs found on {date}.", + "logfileBottomReached": "You have reached the bottom of this logfile.", + "appLogs": "App logs {date}", + "saveLogsToFile": "Save logs to file", + "copyToClipboard": "Copy to clipboard", + "couldNotLoadLogfileFrom": "Could not load logfile from {date}.", + "loadingLogsFrom": "Loading logs from {date}...", + "clearLogs": "Clear logs from {date}", + "showPdf": "Show PDF", + "@showPdf": { + "description": "Tooltip shown on the \"show pdf\" button on the document edit page" + }, + "hidePdf": "Hide PDF", + "@hidePdf": { + "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" + }, + "misc": "Miscellaneous", + "loggingOut": "Logging out...", + "testingConnection": "Testing connection...", + "@testingConnection": { + "description": "Text shown while the app tries to establish a connection to the specified host." + }, + "version": "Version {versionCode}", + "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", + "addNote": "Add note", + "newerVersionAvailable": "Newer version available:" +} \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 5b9ea09..284645a 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1026,5 +1026,6 @@ }, "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "addNote": "Add note", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index b84fa77..c98f9de 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1026,5 +1026,6 @@ }, "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", - "addNote": "Add note" + "addNote": "Add note", + "newerVersionAvailable": "Newer version available:" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 8d3fc05..c1ed3d8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -408,6 +408,7 @@ class _GoRouterShellState extends State { Locale('pl'), Locale('ru'), Locale('tr'), + Locale('it'), ], localeResolutionCallback: (locale, supportedLocales) { if (locale == null) { diff --git a/packages/paperless_api/lib/src/models/paperless_server_information_model.dart b/packages/paperless_api/lib/src/models/paperless_server_information_model.dart index 73bb983..e37409a 100644 --- a/packages/paperless_api/lib/src/models/paperless_server_information_model.dart +++ b/packages/paperless_api/lib/src/models/paperless_server_information_model.dart @@ -4,6 +4,7 @@ class PaperlessServerInformationModel { static const String versionHeader = 'x-version'; static const String apiVersionHeader = 'x-api-version'; final String version; + final String latestVersion; final int apiVersion; final bool isUpdateAvailable; @@ -11,9 +12,11 @@ class PaperlessServerInformationModel { required this.version, required this.apiVersion, required this.isUpdateAvailable, + required this.latestVersion, }); int compareToOtherVersion(String other) { - return getExtendedVersionNumber(version).compareTo(getExtendedVersionNumber(other)); + return getExtendedVersionNumber(version) + .compareTo(getExtendedVersionNumber(other)); } } diff --git a/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart b/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart index fccf1f0..8bafe85 100644 --- a/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/server_stats_api/paperless_server_stats_api_impl.dart @@ -24,13 +24,15 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi { "/api/remote_version/", options: Options(validateStatus: (status) => status == 200), ); - var version = response.data["version"] as String; - if (version == _fallbackVersion) { - version = response.headers.value('x-version') ?? _fallbackVersion; - } + final latestVersion = response.data["version"] as String; + final version = response.headers + .value(PaperlessServerInformationModel.versionHeader) ?? + _fallbackVersion; final updateAvailable = response.data["update_available"] as bool; return PaperlessServerInformationModel( - apiVersion: int.parse(response.headers.value('x-api-version')!), + apiVersion: int.parse(response.headers + .value(PaperlessServerInformationModel.apiVersionHeader)!), + latestVersion: latestVersion, version: version, isUpdateAvailable: updateAvailable, ); From c7d3d9207b18d1a67a51361b7429ac1696c1dabc Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Wed, 3 Jan 2024 19:07:05 +0100 Subject: [PATCH 5/8] feat: Update translations --- lib/l10n/intl_ca.arb | 16 +++++++++++++++- lib/l10n/intl_cs.arb | 16 +++++++++++++++- lib/l10n/intl_de.arb | 16 +++++++++++++++- lib/l10n/intl_en.arb | 17 ++++++++++++++++- lib/l10n/intl_es.arb | 16 +++++++++++++++- lib/l10n/intl_fr.arb | 16 +++++++++++++++- lib/l10n/intl_it.arb | 16 +++++++++++++++- lib/l10n/intl_nl.arb | 16 +++++++++++++++- lib/l10n/intl_pl.arb | 16 +++++++++++++++- lib/l10n/intl_ro.arb | 16 +++++++++++++++- lib/l10n/intl_ru.arb | 16 +++++++++++++++- lib/l10n/intl_tr.arb | 16 +++++++++++++++- scripts/upload_translation_source.sh | 10 ++++++++++ 13 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 scripts/upload_translation_source.sh diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb index 1fbf9eb..2ddcf99 100644 --- a/lib/l10n/intl_ca.arb +++ b/lib/l10n/intl_ca.arb @@ -1027,5 +1027,19 @@ "version": "Versió {versionCode}", "notes": "{count, plural, zero{Notes} one{Nota} other{Notes}}", "addNote": "Afegir Nota", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 3a9ddb7..b41ea14 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", "addNote": "Add note", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 35f699e..58f60aa 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notizen} one{Notiz} other{Notizen}}", "addNote": "Notiz hinzufügen", - "newerVersionAvailable": "Neuere Version verfügbar:" + "newerVersionAvailable": "Neuere Version verfügbar:", + "dateOutOfRange": "Das Datum muss zwischen {firstDate} und {lastDate} liegen.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fab84f1..b00b127 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1027,5 +1027,20 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", "addNote": "Add note", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + + } } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 63ab94a..eefdf87 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notas} one{Nota} other{Notas}}", "addNote": "Añadir nota", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index be1d4a1..e18bd29 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", "addNote": "Ajouter une note", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index a46990c..11455a8 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1027,5 +1027,19 @@ "version": "Versione {versionCode}", "notes": "{count, plural, zero{Nota} one{Nota} other{Note}}", "addNote": "Aggiungi nota", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index fab84f1..d647c74 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", "addNote": "Add note", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index d03b374..10a797f 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", "addNote": "Add note", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index fab84f1..d647c74 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", "addNote": "Add note", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 284645a..f102355 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", "addNote": "Add note", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index c98f9de..331ea52 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1027,5 +1027,19 @@ "version": "Version {versionCode}", "notes": "{count, plural, zero{Notes} one{Note} other{Notes}}", "addNote": "Add note", - "newerVersionAvailable": "Newer version available:" + "newerVersionAvailable": "Newer version available:", + "dateOutOfRange": "Date must be between {firstDate} and {lastDate}.", + "@dateOutOfRange": { + "description": "Error message shown when the user tries to select a date outside of the allowed range.", + "placeholders": { + "firstDate": { + "type": "DateTime", + "format": "yMd" + }, + "lastDate": { + "type": "DateTime", + "format": "yMd" + } + } + } } \ No newline at end of file diff --git a/scripts/upload_translation_source.sh b/scripts/upload_translation_source.sh new file mode 100644 index 0000000..2c4a374 --- /dev/null +++ b/scripts/upload_translation_source.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +__script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +readonly __script_dir + +cd "$__script_dir/../" +echo "Uploading source translation file..." +crowdin upload sources --identity=crowdin_credentials.yml --preserve-hierarchy +flutter packages pub get \ No newline at end of file From ddd950a8dabd7fe9473627f5fb88361ebad8318e Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Wed, 3 Jan 2024 20:22:28 +0100 Subject: [PATCH 6/8] feat: Finalize notes feature, update translations --- android/app/src/main/AndroidManifest.xml | 14 +- lib/core/database/hive/hive_config.dart | 2 + lib/core/database/hive/hive_extensions.dart | 1 + .../form_builder_localized_date_picker.dart | 23 +-- lib/core/widgets/hint_card.dart | 2 +- lib/core/widgets/hint_state_builder.dart | 24 +++ .../view/pages/document_details_page.dart | 31 +++- .../view/widgets/document_notes_widget.dart | 143 ++++++++++++------ .../view/document_edit_page.dart | 2 +- .../view/document_search_bar.dart | 2 +- .../document_upload_preparation_page.dart | 2 +- .../documents/view/pages/documents_page.dart | 15 +- lib/l10n/intl_ca.arb | 5 +- lib/l10n/intl_cs.arb | 5 +- lib/l10n/intl_de.arb | 5 +- lib/l10n/intl_en.arb | 6 +- lib/l10n/intl_es.arb | 5 +- lib/l10n/intl_fr.arb | 5 +- lib/l10n/intl_it.arb | 5 +- lib/l10n/intl_nl.arb | 5 +- lib/l10n/intl_pl.arb | 5 +- lib/l10n/intl_ro.arb | 5 +- lib/l10n/intl_ru.arb | 5 +- lib/l10n/intl_tr.arb | 5 +- lib/main.dart | 1 + lib/theme.dart | 4 +- pubspec.lock | 64 +++++--- pubspec.yaml | 4 +- 28 files changed, 281 insertions(+), 114 deletions(-) create mode 100644 lib/core/widgets/hint_state_builder.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4001596..9498ab3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true" - > + > + @@ -162,11 +163,22 @@ + + + + + + + + + + + get all => [ globalSettings, localUserCredentials, localUserAccount, localUserAppState, + hintStateBox, hosts, ]; } diff --git a/lib/core/database/hive/hive_extensions.dart b/lib/core/database/hive/hive_extensions.dart index 6288eac..e3b2193 100644 --- a/lib/core/database/hive/hive_extensions.dart +++ b/lib/core/database/hive/hive_extensions.dart @@ -54,4 +54,5 @@ extension HiveBoxAccessors on HiveInterface { box(HiveBoxes.localUserAppState); Box get globalSettingsBox => box(HiveBoxes.globalSettings); + Box get hintStateBox => box(HiveBoxes.hintStateBox); } diff --git a/lib/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart b/lib/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart index eb1ab5f..6fb020c 100644 --- a/lib/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart +++ b/lib/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart @@ -3,9 +3,11 @@ import 'dart:collection'; import 'package:collection/collection.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart'; @@ -83,7 +85,6 @@ class _FormBuilderLocalizedDatePickerState final _textFieldControls = LinkedList<_NeighbourAwareDateInputSegmentControls>(); - String? _error; bool _temporarilyDisableListeners = false; @override void initState() { @@ -184,10 +185,7 @@ class _FormBuilderLocalizedDatePickerState // Imitate the functionality of the validator function in "normal" form fields. // The error is shown on the outer decorator as if this was a regular text input. // Errors are cleared after the next user interaction. - final error = _validateDate(value); - setState(() { - _error = error; - }); + // final error = _validateDate(value); }, autovalidateMode: AutovalidateMode.onUserInteraction, initialValue: widget.initialValue != null @@ -201,7 +199,7 @@ class _FormBuilderLocalizedDatePickerState child: InputDecorator( textAlignVertical: TextAlignVertical.bottom, decoration: InputDecoration( - errorText: _error, + errorText: field.errorText, labelText: widget.labelText, suffixIcon: Row( mainAxisSize: MainAxisSize.min, @@ -271,16 +269,10 @@ class _FormBuilderLocalizedDatePickerState if (d.day != date.day && d.month != date.month && d.year != date.year) { return "Invalid date."; } - if (d.isBefore(widget.firstDate)) { - final formattedDateHint = - DateFormat.yMd(widget.locale.toString()).format(widget.firstDate); - return "Date must be after $formattedDateHint."; - } - if (d.isAfter(widget.lastDate)) { - final formattedDateHint = - DateFormat.yMd(widget.locale.toString()).format(widget.lastDate); - return "Date must be before $formattedDateHint."; + if (d.isBefore(widget.firstDate) || d.isAfter(widget.lastDate)) { + return S.of(context)!.dateOutOfRange(widget.firstDate, widget.lastDate); } + return null; } @@ -332,6 +324,7 @@ class _FormBuilderLocalizedDatePickerState _DateInputSegment.year => fieldValue.copyWith(year: number), }; field.setValue(newValue); + field.validate(); } }, inputFormatters: [ diff --git a/lib/core/widgets/hint_card.dart b/lib/core/widgets/hint_card.dart index 195473f..7a94df3 100644 --- a/lib/core/widgets/hint_card.dart +++ b/lib/core/widgets/hint_card.dart @@ -61,7 +61,7 @@ class HintCard extends StatelessWidget { const Padding(padding: EdgeInsets.only(bottom: 24)), ], ).padded(), - ).padded(), + ), ); } } diff --git a/lib/core/widgets/hint_state_builder.dart b/lib/core/widgets/hint_state_builder.dart new file mode 100644 index 0000000..22a636e --- /dev/null +++ b/lib/core/widgets/hint_state_builder.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_mobile/core/database/hive/hive_extensions.dart'; + +class HintStateBuilder extends StatelessWidget { + final String? listenKey; + final Widget Function(BuildContext context, Box box) builder; + const HintStateBuilder({ + super.key, + required this.builder, + this.listenKey, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: Hive.hintStateBox + .listenable(keys: listenKey != null ? [listenKey] : null), + builder: (context, box, child) { + return builder(context, box); + }, + ); + } +} diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 0052df8..d0f7301 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -161,6 +161,7 @@ class _DocumentDetailsPageState extends State { bottom: ColoredTabBar( tabBar: TabBar( isScrollable: true, + tabAlignment: TabAlignment.start, tabs: [ Tab( child: Text( @@ -203,19 +204,33 @@ class _DocumentDetailsPageState extends State { ), ), Tab( - child: Text( - "Notes", - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + S.of(context)!.notes(0), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + if ((state.document?.notes.length ?? 0) > + 0) + Card( + child: Text(state + .document!.notes.length + .toString()) + .paddedSymmetrically( + horizontal: 8, vertical: 2), + ), + ], ), ), if (hasMultiUserSupport) Tab( child: Text( - "Permissions", + S.of(context)!.permissions, style: TextStyle( color: Theme.of(context) .colorScheme diff --git a/lib/features/document_details/view/widgets/document_notes_widget.dart b/lib/features/document_details/view/widgets/document_notes_widget.dart index 189e373..89048e1 100644 --- a/lib/features/document_details/view/widgets/document_notes_widget.dart +++ b/lib/features/document_details/view/widgets/document_notes_widget.dart @@ -1,12 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/core/widgets/hint_card.dart'; +import 'package:paperless_mobile/core/widgets/hint_state_builder.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:markdown/markdown.dart' show markdownToHtml; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class DocumentNotesWidget extends StatefulWidget { final DocumentModel document; @@ -19,11 +28,29 @@ class DocumentNotesWidget extends StatefulWidget { class _DocumentNotesWidgetState extends State { final _noteContentController = TextEditingController(); final _formKey = GlobalKey(); - + bool _isNoteSubmitting = false; @override Widget build(BuildContext context) { + const hintKey = "hideMarkdownSyntaxHint"; return SliverMainAxisGroup( slivers: [ + SliverPadding( + padding: const EdgeInsets.only(bottom: 16), + sliver: SliverToBoxAdapter( + child: HintStateBuilder( + listenKey: hintKey, + builder: (context, box) { + return HintCard( + hintText: S.of(context)!.notesMarkdownSyntaxSupportHint, + show: !box.get(hintKey, defaultValue: false)!, + onHintAcknowledged: () { + box.put(hintKey, true); + }, + ); + }, + ), + ), + ), SliverToBoxAdapter( child: Form( key: _formKey, @@ -33,79 +60,107 @@ class _DocumentNotesWidgetState extends State { controller: _noteContentController, maxLines: null, validator: (value) { - if (value?.isEmpty ?? true) { + if (value?.trim().isEmpty ?? true) { return S.of(context)!.thisFieldIsRequired; } return null; }, + textInputAction: TextInputAction.newline, decoration: InputDecoration( - hintText: 'Your note here...', - labelText: 'New note', - floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: S.of(context)!.newNote, + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _noteContentController.clear(); + }, + ), ), - ).padded(), + ).paddedOnly(bottom: 8), Align( alignment: Alignment.centerRight, - child: FilledButton.icon( - icon: Icon(Icons.note_add_outlined), - label: Text("Add note"), - onPressed: () { + child: ElevatedButton.icon( + icon: _isNoteSubmitting + ? const SizedBox.square( + dimension: 20, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 3, + ), + ), + ) + : const Icon(Icons.note_add_outlined), + label: Text(S.of(context)!.addNote), + onPressed: () async { _formKey.currentState?.save(); if (_formKey.currentState?.validate() ?? false) { - context - .read() - .addNote(_noteContentController.text); + setState(() { + _isNoteSubmitting = true; + }); + try { + await context + .read() + .addNote(_noteContentController.text.trim()); + _noteContentController.clear(); + } catch (error) { + showGenericError(context, error); + } finally { + setState(() { + _isNoteSubmitting = false; + }); + } } }, - ).padded(), + ), ), ], - ).padded(), + ), ), ), + const SliverToBoxAdapter( + child: SizedBox(height: 16), + ), SliverList.separated( separatorBuilder: (context, index) => const SizedBox(height: 16), itemBuilder: (context, index) { final note = widget.document.notes.elementAt(index); return Card( - // borderRadius: BorderRadius.circular(8), - // elevation: 1, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (note.created != null) - Text( - DateFormat.yMMMd( - Localizations.localeOf(context).toString()) - .addPattern('\u2014') - .add_jm() - .format(note.created!), - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(.5), - ), - ), - const SizedBox(height: 8), - Text( - note.note!, - textAlign: TextAlign.justify, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), + Html( + data: markdownToHtml(note.note!), + onLinkTap: (url, attributes, element) async { + if (url?.isEmpty ?? true) { + return; + } + if (await canLaunchUrlString(url!)) { + launchUrlString(url); + } + }, ), Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + if (note.created != null) + Text( + DateFormat.yMMMd( + Localizations.localeOf(context).toString()) + .addPattern('\u2014') + .add_jm() + .format(note.created!), + style: + Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(.5), + ), + ), IconButton( - icon: Icon(Icons.delete), + tooltip: S.of(context)!.delete, + icon: const Icon(Icons.delete), onPressed: () { context.read().deleteNote(note); - showSnackBar( - context, - S.of(context)!.documentSuccessfullyUpdated, - ); }, ), ], diff --git a/lib/features/document_edit/view/document_edit_page.dart b/lib/features/document_edit/view/document_edit_page.dart index eaa16a9..8631d72 100644 --- a/lib/features/document_edit/view/document_edit_page.dart +++ b/lib/features/document_edit/view/document_edit_page.dart @@ -424,7 +424,7 @@ class _DocumentEditPageState extends State initialValue: initialCreatedAtDate, labelText: S.of(context)!.createdAt, firstDate: DateTime(1970, 1, 1), - lastDate: DateTime.now(), + lastDate: DateTime(2100, 1, 1), locale: Localizations.localeOf(context), prefixIcon: Icon(Icons.calendar_today), ), diff --git a/lib/features/document_search/view/document_search_bar.dart b/lib/features/document_search/view/document_search_bar.dart index 43b81ef..c546439 100644 --- a/lib/features/document_search/view/document_search_bar.dart +++ b/lib/features/document_search/view/document_search_bar.dart @@ -40,7 +40,7 @@ class _DocumentSearchBarState extends State { constraints: const BoxConstraints( maxWidth: 720, minWidth: 360, - maxHeight: 56, + maxHeight: 48, minHeight: 48, ), child: Row( diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index 4ab0df6..d605122 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -222,7 +222,7 @@ class _DocumentUploadPreparationPageState FormBuilderLocalizedDatePicker( name: DocumentModel.createdKey, firstDate: DateTime(1970, 1, 1), - lastDate: DateTime.now(), + lastDate: DateTime(2100, 1, 1), locale: Localizations.localeOf(context), labelText: S.of(context)!.createdAt + " *", allowUnset: true, diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index c1cc561..7b86f63 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -21,6 +21,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/docum import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; +import 'package:paperless_mobile/features/logging/data/logger.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; @@ -308,8 +309,18 @@ class _DocumentsPageState extends State { // Listen for scroll notifications to load new data. // Scroll controller does not work here due to nestedscrollview limitations. final offset = notification.metrics.pixels; - if (offset > 128 && _savedViewsExpansionController.isExpanded) { - _savedViewsExpansionController.collapse(); + try { + if (offset > 128 && _savedViewsExpansionController.isExpanded) { + _savedViewsExpansionController.collapse(); + } + // Workaround for https://github.com/astubenbord/paperless-mobile/issues/341 probably caused by https://github.com/flutter/flutter/issues/138153 + } on TypeError catch (error) { + logger.fw( + "An exception was thrown, but this message can probably be ignored. See issue #341 for more details.", + error: error, + className: runtimeType.toString(), + methodName: "_buildDocumentsTab", + ); } final max = notification.metrics.maxScrollExtent; diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb index 2ddcf99..878c73c 100644 --- a/lib/l10n/intl_ca.arb +++ b/lib/l10n/intl_ca.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index b41ea14..2a2634e 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 58f60aa..8453eb2 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Berechtigungen", + "newNote": "Neue Notiz", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile unterstützt Markdown-Syntax zur Darstellung und Formatierung von Notizen. Probiere es aus!" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b00b127..4f8f10b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1041,6 +1041,8 @@ "format": "yMd" } } - - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index eefdf87..b34def0 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index e18bd29..d73fa3c 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 11455a8..5e3605d 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index d647c74..4f8f10b 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 10a797f..b55d093 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index d647c74..4f8f10b 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index f102355..cb7997b 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 331ea52..3f3ef00 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1041,5 +1041,8 @@ "format": "yMd" } } - } + }, + "permissions": "Permissions", + "newNote": "New note", + "notesMarkdownSyntaxSupportHint": "Paperless Mobile can render notes using basic markdown syntax. Try it out!" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index c1ed3d8..f88b028 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -111,6 +111,7 @@ Future _initHive() async { registerHiveAdapters(); await Hive.openBox(HiveBoxes.localUserAccount); await Hive.openBox(HiveBoxes.localUserAppState); + await Hive.openBox(HiveBoxes.hintStateBox); await Hive.openBox(HiveBoxes.hosts); final globalSettingsBox = await Hive.openBox(HiveBoxes.globalSettings); diff --git a/lib/theme.dart b/lib/theme.dart index 7331415..84771fc 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -82,14 +82,14 @@ SystemUiOverlayStyle buildOverlayStyle( Brightness.light => SystemUiOverlayStyle.dark.copyWith( systemNavigationBarColor: color, systemNavigationBarDividerColor: color, - statusBarColor: theme.colorScheme.background, + // statusBarColor: theme.colorScheme.background, // statusBarColor: theme.colorScheme.background, // systemNavigationBarDividerColor: theme.colorScheme.surface, ), Brightness.dark => SystemUiOverlayStyle.light.copyWith( systemNavigationBarColor: color, systemNavigationBarDividerColor: color, - statusBarColor: theme.colorScheme.background, + // statusBarColor: theme.colorScheme.background, // statusBarColor: theme.colorScheme.background, // systemNavigationBarDividerColor: theme.colorScheme.surface, ), diff --git a/pubspec.lock b/pubspec.lock index 9746de5..7182728 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -221,10 +221,10 @@ packages: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" color: dependency: transitive description: @@ -848,6 +848,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + http_mock_adapter: + dependency: transitive + description: + name: http_mock_adapter + sha256: "46399c78bd4a0af071978edd8c502d7aeeed73b5fb9860bca86b5ed647a63c1b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" http_multi_server: dependency: transitive description: @@ -1014,7 +1022,7 @@ packages: source: hosted version: "1.2.0" markdown: - dependency: transitive + dependency: "direct main" description: name: markdown sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd @@ -1041,10 +1049,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -1060,6 +1068,14 @@ packages: relative: true source: path version: "0.0.1" + mockito: + dependency: transitive + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" mocktail: dependency: transitive description: @@ -1223,8 +1239,8 @@ packages: dependency: "direct main" description: path: "packages/pdfx" - ref: HEAD - resolved-ref: "11f7dee82b58ca4f483c753f06bbdc91b34a0793" + ref: "4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b" + resolved-ref: "4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b" url: "https://github.com/ScerIO/packages.flutter" source: git version: "2.5.0" @@ -1288,10 +1304,10 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: @@ -1637,18 +1653,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1693,26 +1709,26 @@ packages: dependency: transitive description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" time: dependency: transitive description: @@ -1877,10 +1893,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "11.7.1" + version: "11.10.0" watcher: dependency: transitive description: @@ -1893,10 +1909,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: "direct main" description: @@ -1994,5 +2010,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5cce3e5..081c8bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -103,8 +103,10 @@ dependencies: # camerawesome: ^2.0.0-dev.1 pdfx: git: - url: "https://github.com/ScerIO/packages.flutter" + url: 'https://github.com/ScerIO/packages.flutter' + ref: '4be9de9ffed5398fd7d5f44bbb07dcd3d3f1711b' path: packages/pdfx + markdown: ^7.1.1 dependency_overrides: intl: ^0.18.1 From 497777c52b41703188446d0ed296fa4f651b396e Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 6 Jan 2024 19:23:30 +0100 Subject: [PATCH 7/8] feat: Add login integration test (WIP), update notes feature --- .../android/de-DE/changelogs/4043.txt | 2 + build.yaml | 16 + integration_test/login_integration_test.dart | 316 ++++++------------ integration_test/src/framework.dart | 17 +- .../src/mocks/mock_paperless_api.dart | 65 ++++ .../database/hive/hive_initialization.dart | 25 ++ .../language_header.interceptor.dart | 8 +- lib/core/security/session_manager.dart | 87 +---- lib/core/security/session_manager_impl.dart | 96 ++++++ .../service/connectivity_status_service.dart | 3 +- .../view/widgets/document_notes_widget.dart | 2 + .../login/cubit/authentication_cubit.dart | 15 +- lib/features/login/view/add_account_page.dart | 3 +- .../server_address_form_field.dart | 3 +- lib/keys.dart | 19 ++ lib/main.dart | 172 +++++----- lib/routing/routes/login_route.dart | 2 + lib/theme.dart | 4 +- .../lib/src/models/document_model.dart | 2 +- .../example/pubspec.lock | 26 +- 20 files changed, 465 insertions(+), 418 deletions(-) create mode 100644 android/fastlane/metadata/android/de-DE/changelogs/4043.txt create mode 100644 build.yaml create mode 100644 integration_test/src/mocks/mock_paperless_api.dart create mode 100644 lib/core/database/hive/hive_initialization.dart create mode 100644 lib/core/security/session_manager_impl.dart create mode 100644 lib/keys.dart diff --git a/android/fastlane/metadata/android/de-DE/changelogs/4043.txt b/android/fastlane/metadata/android/de-DE/changelogs/4043.txt new file mode 100644 index 0000000..f591cc4 --- /dev/null +++ b/android/fastlane/metadata/android/de-DE/changelogs/4043.txt @@ -0,0 +1,2 @@ +- New feature: Notes +- Several bugfixes diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..57dfbd7 --- /dev/null +++ b/build.yaml @@ -0,0 +1,16 @@ +targets: + $default: + include: + - pubspec.yaml + sources: + - assets/** + - lib/$lib$ + - lib/**.dart + - test/**.dart + - integration_test/**.dart + + builders: + mockito|mockBuilder: + generate_for: + - test/**.dart + - integration_test/**.dart \ No newline at end of file diff --git a/integration_test/login_integration_test.dart b/integration_test/login_integration_test.dart index 8f59b03..285d47b 100644 --- a/integration_test/login_integration_test.dart +++ b/integration_test/login_integration_test.dart @@ -1,224 +1,130 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_test/flutter_test.dart'; -// import 'package:mockito/mockito.dart'; -// import 'package:paperless_api/paperless_api.dart'; -// import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -// import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; -// import 'package:paperless_mobile/core/store/local_vault.dart'; -// import 'package:paperless_mobile/di_test_mocks.mocks.dart'; -// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; -// import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; -// import 'package:paperless_mobile/features/settings/model/view_type.dart'; +import 'dart:io'; -// import 'src/framework.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:paperless_mobile/core/database/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/hive/hive_extensions.dart'; +import 'package:paperless_mobile/core/database/hive/hive_initialization.dart'; +import 'package:paperless_mobile/core/database/tables/global_settings.dart'; +import 'package:paperless_mobile/core/security/session_manager.dart'; +import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; +import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; +import 'package:paperless_mobile/features/login/services/authentication_service.dart'; +import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; +import 'package:paperless_mobile/keys.dart'; +import 'package:paperless_mobile/main.dart' + show initializeDefaultParameters, AppEntrypoint; +import 'package:path_provider/path_provider.dart'; -// void main() async { -// final t = await initializeTestingFramework(languageCode: 'de'); +import 'src/mocks/mock_paperless_api.dart'; -// const testServerUrl = 'https://example.com'; -// const testUsername = 'user'; -// const testPassword = 'pass'; +class MockConnectivityStatusService extends Mock + implements ConnectivityStatusService {} -// final serverAddressField = find.byKey(const ValueKey('login-server-address')); -// final usernameField = find.byKey(const ValueKey('login-username')); -// final passwordField = find.byKey(const ValueKey('login-password')); -// final loginBtn = find.byKey(const ValueKey('login-login-button')); +class MockLocalAuthService extends Mock implements LocalAuthenticationService {} -// testWidgets('Test successful login flow', (WidgetTester tester) async { -// await initAndLaunchTestApp(tester, () async { -// // Initialize dat for mocked classes -// when((getIt()).connectivityChanges()) -// .thenAnswer((i) => Stream.value(true)); -// when((getIt() as MockLocalVault) -// .loadAuthenticationInformation()) -// .thenAnswer((realInvocation) async => null); -// when((getIt() as MockLocalVault).loadApplicationSettings()) -// .thenAnswer((realInvocation) async => ApplicationSettingsState( -// preferredLocaleSubtag: 'en', -// preferredThemeMode: ThemeMode.light, -// isLocalAuthenticationEnabled: false, -// preferredViewType: ViewType.list, -// showInboxOnStartup: false, -// )); -// when(getIt().login( -// username: testUsername, -// password: testPassword, -// )).thenAnswer((i) => Future.value("eyTestToken")); +class MockSessionManager extends Mock implements SessionManager {} -// await getIt().initialize(); -// await getIt().initialize(); -// }); +class MockLocalNotificationService extends Mock + implements LocalNotificationService {} -// // Mocked classes +void main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + const locale = Locale("en", "US"); + const testServerUrl = 'https://example.com'; + const testUsername = 'user'; + const testPassword = 'pass'; -// await t.binding.waitUntilFirstFrameRasterized; -// await tester.pumpAndSettle(); + final hiveDirectory = await getTemporaryDirectory(); -// await tester.enterText(serverAddressField, testServerUrl); -// await tester.pumpAndSettle(); + late ConnectivityStatusService connectivityStatusService; + late MockPaperlessApiFactory paperlessApiFactory; + late AuthenticationCubit authenticationCubit; + late LocalNotificationService localNotificationService; + late SessionManager sessionManager; + final localAuthService = MockLocalAuthService(); -// await tester.enterText(usernameField, testUsername); -// await tester.pumpAndSettle(); + setUp(() async { + connectivityStatusService = MockConnectivityStatusService(); + paperlessApiFactory = MockPaperlessApiFactory(); + sessionManager = MockSessionManager(); + localNotificationService = MockLocalNotificationService(); -// await tester.enterText(passwordField, testPassword); + authenticationCubit = AuthenticationCubit( + localAuthService, + paperlessApiFactory, + sessionManager, + connectivityStatusService, + localNotificationService, + ); + await initHive( + hiveDirectory, + locale.toString(), + ); + }); + testWidgets( + 'A user shall be successfully logged in when providing correct credentials.', + (tester) async { + // Reset data to initial state with given [locale]. + await Hive.globalSettingsBox.setValue( + GlobalSettings( + preferredLocaleSubtag: locale.toString(), + loggedInUserId: null, + ), + ); + when(paperlessApiFactory.authenticationApi.login( + username: testUsername, + password: testPassword, + )).thenAnswer((_) async => "token"); -// FocusManager.instance.primaryFocus?.unfocus(); -// await tester.pumpAndSettle(); + await initializeDefaultParameters(); -// await tester.tap(loginBtn); + await tester.pumpWidget( + AppEntrypoint( + apiFactory: paperlessApiFactory, + authenticationCubit: authenticationCubit, + connectivityStatusService: connectivityStatusService, + localNotificationService: localNotificationService, + localAuthService: localAuthService, + sessionManager: sessionManager, + ), + ); + await tester.binding.waitUntilFirstFrameRasterized; + await tester.pumpAndSettle(); -// verify(getIt().login( -// username: testUsername, -// password: testPassword, -// )).called(1); -// }); + await tester.enterText( + find.byKey(TestKeys.login.serverAddressFormField), + testServerUrl, + ); + await tester.pumpAndSettle(); -// testWidgets('Test login validation missing password', -// (WidgetTester tester) async { -// await initAndLaunchTestApp(tester, () async { -// when((getIt() as MockConnectivityStatusService) -// .connectivityChanges()) -// .thenAnswer((i) => Stream.value(true)); -// when((getIt() as MockLocalVault) -// .loadAuthenticationInformation()) -// .thenAnswer((realInvocation) async => null); + await tester.press(find.byKey(TestKeys.login.continueButton)); -// when((getIt() as MockLocalVault).loadApplicationSettings()) -// .thenAnswer((realInvocation) async => ApplicationSettingsState( -// preferredLocaleSubtag: 'en', -// preferredThemeMode: ThemeMode.light, -// isLocalAuthenticationEnabled: false, -// preferredViewType: ViewType.list, -// showInboxOnStartup: false, -// )); + await tester.pumpAndSettle(); + expect( + find.byKey(TestKeys.login.usernameFormField), + findsOneWidget, + ); -// await getIt().initialize(); -// await getIt().initialize(); -// }); -// // Mocked classes + await tester.enterText( + find.byKey(TestKeys.login.usernameFormField), + testUsername, + ); + await tester.enterText( + find.byKey(TestKeys.login.passwordFormField), + testUsername, + ); + await tester.pumpAndSettle(); -// // Initialize dat for mocked classes + await tester.press(find.byKey(TestKeys.login.loginButton)); + await tester.pumpAndSettle(); -// await t.binding.waitUntilFirstFrameRasterized; -// await tester.pumpAndSettle(); - -// await tester.enterText(serverAddressField, testServerUrl); -// await tester.pumpAndSettle(); - -// await tester.enterText(usernameField, testUsername); -// await tester.pumpAndSettle(); - -// FocusManager.instance.primaryFocus?.unfocus(); -// await tester.pumpAndSettle(); - -// await tester.tap(loginBtn); -// await tester.pumpAndSettle(); - -// verifyNever( -// (getIt() as MockPaperlessAuthenticationApi) -// .login( -// username: testUsername, -// password: testPassword, -// )); -// expect( -// find.textContaining(t.translations.passwordMustNotBeEmpty), -// findsOneWidget, -// ); -// }); - -// testWidgets('Test login validation missing username', -// (WidgetTester tester) async { -// await initAndLaunchTestApp(tester, () async { -// when((getIt() as MockConnectivityStatusService) -// .connectivityChanges()) -// .thenAnswer((i) => Stream.value(true)); -// when((getIt() as MockLocalVault) -// .loadAuthenticationInformation()) -// .thenAnswer((realInvocation) async => null); -// when((getIt() as MockLocalVault).loadApplicationSettings()) -// .thenAnswer((realInvocation) async => ApplicationSettingsState( -// preferredLocaleSubtag: 'en', -// preferredThemeMode: ThemeMode.light, -// isLocalAuthenticationEnabled: false, -// preferredViewType: ViewType.list, -// showInboxOnStartup: false, -// )); -// await getIt().initialize(); -// await getIt().initialize(); -// }); - -// await t.binding.waitUntilFirstFrameRasterized; -// await tester.pumpAndSettle(); - -// await tester.enterText(serverAddressField, testServerUrl); -// await tester.pumpAndSettle(); - -// await tester.enterText(passwordField, testPassword); -// await tester.pumpAndSettle(); - -// FocusManager.instance.primaryFocus?.unfocus(); -// await tester.pumpAndSettle(); - -// await tester.tap(loginBtn); -// await tester.pumpAndSettle(); - -// verifyNever( -// (getIt() as MockPaperlessAuthenticationApi) -// .login( -// username: testUsername, -// password: testPassword, -// )); -// expect( -// find.textContaining(t.translations.usernameMustNotBeEmpty), -// findsOneWidget, -// ); -// }); - -// testWidgets('Test login validation missing server address', -// (WidgetTester tester) async { -// initAndLaunchTestApp(tester, () async { -// when((getIt()).connectivityChanges()) -// .thenAnswer((i) => Stream.value(true)); - -// when((getIt()).loadAuthenticationInformation()) -// .thenAnswer((realInvocation) async => null); - -// when((getIt()).loadApplicationSettings()) -// .thenAnswer((realInvocation) async => ApplicationSettingsState( -// preferredLocaleSubtag: 'en', -// preferredThemeMode: ThemeMode.light, -// isLocalAuthenticationEnabled: false, -// preferredViewType: ViewType.list, -// showInboxOnStartup: false, -// )); - -// await getIt().initialize(); -// await getIt().initialize(); -// }); - -// await t.binding.waitUntilFirstFrameRasterized; -// await tester.pumpAndSettle(); - -// await tester.enterText(usernameField, testUsername); -// await tester.pumpAndSettle(); - -// await tester.enterText(passwordField, testPassword); -// await tester.pumpAndSettle(); - -// FocusManager.instance.primaryFocus?.unfocus(); -// await tester.pumpAndSettle(); - -// await tester.tap(loginBtn); -// await tester.pumpAndSettle(); - -// verifyNever(getIt().login( -// username: testUsername, -// password: testPassword, -// )); -// expect( -// find.textContaining( -// t.translations.loginPageServerUrlValidatorMessageText), -// findsOneWidget, -// ); -// }); -// } + expect( + find.byKey(TestKeys.login.loggingInScreen), + findsOneWidget, + ); + }); +} diff --git a/integration_test/src/framework.dart b/integration_test/src/framework.dart index ef95f48..9311c92 100644 --- a/integration_test/src/framework.dart +++ b/integration_test/src/framework.dart @@ -1,7 +1,16 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; +import 'package:paperless_mobile/core/security/session_manager.dart'; +import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; +import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; +import 'package:paperless_mobile/features/login/services/authentication_service.dart'; +import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/main.dart' + show initializeDefaultParameters, AppEntrypoint; +import 'package:path_provider/path_provider.dart'; Future initializeTestingFramework( {String languageCode = 'en'}) async { @@ -26,11 +35,3 @@ class TestingFrameworkVariables { required this.translations, }); } - -Future initAndLaunchTestApp( - WidgetTester tester, - Future Function() initializationCallback, -) async { - await initializationCallback(); - //runApp(const PaperlessMobileEntrypoint(authenticationCubit: ),)); -} diff --git a/integration_test/src/mocks/mock_paperless_api.dart b/integration_test/src/mocks/mock_paperless_api.dart new file mode 100644 index 0000000..81f503e --- /dev/null +++ b/integration_test/src/mocks/mock_paperless_api.dart @@ -0,0 +1,65 @@ +import 'package:dio/src/dio.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; +import 'package:mockito/annotations.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +import 'mock_paperless_api.mocks.dart'; + +class MockPaperlessApiFactory implements PaperlessApiFactory { + final PaperlessAuthenticationApi authenticationApi = + MockPaperlessAuthenticationApi(); + final PaperlessDocumentsApi documentApi = MockPaperlessDocumentsApi(); + final PaperlessLabelsApi labelsApi = MockPaperlessLabelsApi(); + final PaperlessUserApi userApi = MockPaperlessUserApi(); + final PaperlessSavedViewsApi savedViewsApi = MockPaperlessSavedViewsApi(); + final PaperlessServerStatsApi serverStatsApi = MockPaperlessServerStatsApi(); + final PaperlessTasksApi tasksApi = MockPaperlessTasksApi(); + + @override + PaperlessAuthenticationApi createAuthenticationApi(Dio dio) { + return authenticationApi; + } + + @override + PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion}) { + return documentApi; + } + + @override + PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion}) { + return labelsApi; + } + + @override + PaperlessSavedViewsApi createSavedViewsApi( + Dio dio, { + required int apiVersion, + }) { + return savedViewsApi; + } + + @override + PaperlessServerStatsApi createServerStatsApi(Dio dio, + {required int apiVersion}) { + return serverStatsApi; + } + + @override + PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion}) { + return tasksApi; + } + + @override + PaperlessUserApi createUserApi(Dio dio, {required int apiVersion}) { + return userApi; + } +} diff --git a/lib/core/database/hive/hive_initialization.dart b/lib/core/database/hive/hive_initialization.dart new file mode 100644 index 0000000..e5988da --- /dev/null +++ b/lib/core/database/hive/hive_initialization.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +import 'package:hive/hive.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_mobile/core/database/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/global_settings.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; + +Future initHive(Directory directory, String defaultLocale) async { + Hive.init(directory.path); + registerHiveAdapters(); + await Hive.openBox(HiveBoxes.localUserAccount); + await Hive.openBox(HiveBoxes.localUserAppState); + await Hive.openBox(HiveBoxes.hintStateBox); + await Hive.openBox(HiveBoxes.hosts); + final globalSettingsBox = + await Hive.openBox(HiveBoxes.globalSettings); + + if (!globalSettingsBox.hasValue) { + await globalSettingsBox.setValue( + GlobalSettings(preferredLocaleSubtag: defaultLocale), + ); + } +} diff --git a/lib/core/interceptor/language_header.interceptor.dart b/lib/core/interceptor/language_header.interceptor.dart index 4d81272..862d037 100644 --- a/lib/core/interceptor/language_header.interceptor.dart +++ b/lib/core/interceptor/language_header.interceptor.dart @@ -1,16 +1,16 @@ import 'package:dio/dio.dart'; class LanguageHeaderInterceptor extends Interceptor { - String preferredLocaleSubtag; - LanguageHeaderInterceptor(this.preferredLocaleSubtag); + final String Function() preferredLocaleSubtagBuilder; + LanguageHeaderInterceptor(this.preferredLocaleSubtagBuilder); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { late String languages; - if (preferredLocaleSubtag == "en") { + if (preferredLocaleSubtagBuilder() == "en") { languages = "en"; } else { - languages = "$preferredLocaleSubtag,en;q=0.7,en-US;q=0.6"; + languages = "${preferredLocaleSubtagBuilder()},en;q=0.7,en-US;q=0.6"; } options.headers.addAll({"Accept-Language": languages}); handler.next(options); diff --git a/lib/core/security/session_manager.dart b/lib/core/security/session_manager.dart index 7e50889..50877d6 100644 --- a/lib/core/security/session_manager.dart +++ b/lib/core/security/session_manager.dart @@ -1,93 +1,14 @@ -import 'dart:io'; - import 'package:dio/dio.dart'; -import 'package:dio/io.dart'; import 'package:flutter/material.dart'; -import 'package:paperless_api/src/interceptor/dio_http_error_interceptor.dart'; -import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart'; -import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart'; -import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -import 'package:pretty_dio_logger/pretty_dio_logger.dart'; -/// Manages the security context, authentication and base request URL for -/// an underlying [Dio] client which is injected into all services -/// requiring authenticated access to the Paperless REST API. -class SessionManager extends ValueNotifier { - Dio get client => value; - - SessionManager([List interceptors = const []]) - : super(_initDio(interceptors)); - - static Dio _initDio(List interceptors) { - //en- and decoded by utf8 by default - final Dio dio = Dio( - BaseOptions( - contentType: Headers.jsonContentType, - followRedirects: true, - maxRedirects: 10, - ), - ); - dio.options - ..receiveTimeout = const Duration(seconds: 30) - ..sendTimeout = const Duration(seconds: 60) - ..responseType = ResponseType.json; - (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = - () => HttpClient()..badCertificateCallback = (cert, host, port) => true; - dio.interceptors.addAll([ - ...interceptors, - DioUnauthorizedInterceptor(), - DioHttpErrorInterceptor(), - DioOfflineInterceptor(), - RetryOnConnectionChangeInterceptor(dio: dio) - ]); - return dio; - } +abstract interface class SessionManager implements ChangeNotifier { + Dio get client; void updateSettings({ String? baseUrl, String? authToken, ClientCertificate? clientCertificate, - }) { - if (clientCertificate != null) { - final context = SecurityContext() - ..usePrivateKeyBytes( - clientCertificate.bytes, - password: clientCertificate.passphrase, - ) - ..useCertificateChainBytes( - clientCertificate.bytes, - password: clientCertificate.passphrase, - ) - ..setTrustedCertificatesBytes( - clientCertificate.bytes, - password: clientCertificate.passphrase, - ); - final adapter = IOHttpClientAdapter() - ..createHttpClient = () => HttpClient(context: context) - ..badCertificateCallback = - (X509Certificate cert, String host, int port) => true; - - client.httpClientAdapter = adapter; - } - - if (baseUrl != null) { - client.options.baseUrl = baseUrl; - } - - if (authToken != null) { - client.options.headers.addAll({ - HttpHeaders.authorizationHeader: 'Token $authToken', - }); - } - - notifyListeners(); - } - - void resetSettings() { - client.httpClientAdapter = IOHttpClientAdapter(); - client.options.baseUrl = ''; - client.options.headers.remove(HttpHeaders.authorizationHeader); - notifyListeners(); - } + }); + void resetSettings(); } diff --git a/lib/core/security/session_manager_impl.dart b/lib/core/security/session_manager_impl.dart new file mode 100644 index 0000000..c9a5491 --- /dev/null +++ b/lib/core/security/session_manager_impl.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; +import 'package:flutter/material.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart'; +import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart'; +import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; +import 'package:paperless_mobile/core/security/session_manager.dart'; +import 'package:paperless_mobile/features/login/model/client_certificate.dart'; + +/// Manages the security context, authentication and base request URL for +/// an underlying [Dio] client which is injected into all services +/// requiring authenticated access to the Paperless REST API. +class SessionManagerImpl extends ValueNotifier implements SessionManager { + @override + Dio get client => value; + + SessionManagerImpl([List interceptors = const []]) + : super(_initDio(interceptors)); + + static Dio _initDio(List interceptors) { + //en- and decoded by utf8 by default + final Dio dio = Dio( + BaseOptions( + contentType: Headers.jsonContentType, + followRedirects: true, + maxRedirects: 10, + ), + ); + dio.options + ..receiveTimeout = const Duration(seconds: 30) + ..sendTimeout = const Duration(seconds: 60) + ..responseType = ResponseType.json; + (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = + () => HttpClient()..badCertificateCallback = (cert, host, port) => true; + dio.interceptors.addAll([ + ...interceptors, + DioUnauthorizedInterceptor(), + DioHttpErrorInterceptor(), + DioOfflineInterceptor(), + RetryOnConnectionChangeInterceptor(dio: dio) + ]); + return dio; + } + + @override + void updateSettings({ + String? baseUrl, + String? authToken, + ClientCertificate? clientCertificate, + }) { + if (clientCertificate != null) { + final context = SecurityContext() + ..usePrivateKeyBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ) + ..useCertificateChainBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ) + ..setTrustedCertificatesBytes( + clientCertificate.bytes, + password: clientCertificate.passphrase, + ); + final adapter = IOHttpClientAdapter() + ..createHttpClient = () => HttpClient(context: context) + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + + client.httpClientAdapter = adapter; + } + + if (baseUrl != null) { + client.options.baseUrl = baseUrl; + } + + if (authToken != null) { + client.options.headers.addAll({ + HttpHeaders.authorizationHeader: 'Token $authToken', + }); + } + + notifyListeners(); + } + + @override + void resetSettings() { + client.httpClientAdapter = IOHttpClientAdapter(); + client.options.baseUrl = ''; + client.options.headers.remove(HttpHeaders.authorizationHeader); + notifyListeners(); + } +} diff --git a/lib/core/service/connectivity_status_service.dart b/lib/core/service/connectivity_status_service.dart index 6ce404b..0e95a2f 100644 --- a/lib/core/service/connectivity_status_service.dart +++ b/lib/core/service/connectivity_status_service.dart @@ -5,6 +5,7 @@ import 'package:dio/dio.dart'; import 'package:paperless_mobile/core/global/os_error_codes.dart'; import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart'; import 'package:paperless_mobile/core/security/session_manager.dart'; +import 'package:paperless_mobile/core/security/session_manager_impl.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart'; import 'package:rxdart/subjects.dart'; @@ -79,7 +80,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService { } try { SessionManager manager = - SessionManager([ServerReachabilityErrorInterceptor()]) + SessionManagerImpl([ServerReachabilityErrorInterceptor()]) ..updateSettings(clientCertificate: clientCertificate) ..client.options.connectTimeout = const Duration(seconds: 5) ..client.options.receiveTimeout = const Duration(seconds: 5); diff --git a/lib/features/document_details/view/widgets/document_notes_widget.dart b/lib/features/document_details/view/widgets/document_notes_widget.dart index 89048e1..3fc50f3 100644 --- a/lib/features/document_details/view/widgets/document_notes_widget.dart +++ b/lib/features/document_details/view/widgets/document_notes_widget.dart @@ -92,6 +92,8 @@ class _DocumentNotesWidgetState extends State { label: Text(S.of(context)!.addNote), onPressed: () async { _formKey.currentState?.save(); + FocusScope.of(context).unfocus(); + if (_formKey.currentState?.validate() ?? false) { setState(() { _isNoteSubmitting = true; diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index 121e779..3263750 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/transient_error.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/hive/hive_extensions.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; @@ -13,6 +14,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_settings.dart'; import 'package:paperless_mobile/core/database/tables/user_credentials.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; +import 'package:paperless_mobile/core/security/session_manager_impl.dart'; import 'package:paperless_mobile/features/logging/data/logger.dart'; import 'package:paperless_mobile/features/logging/utils/redaction_utils.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart'; @@ -83,7 +85,7 @@ class AuthenticationCubit extends Cubit { AuthenticatingStage.persistingLocalUserData)); }, ); - } catch (e) { + } on PaperlessApiException catch (exception, stackTrace) { emit( AuthenticationErrorState( serverUrl: serverUrl, @@ -207,8 +209,8 @@ class AuthenticationCubit extends Cubit { methodName: 'switchAccount', ); - final sessionManager = SessionManager([ - LanguageHeaderInterceptor(locale), + final SessionManager sessionManager = SessionManagerImpl([ + LanguageHeaderInterceptor(() => locale), ]); await _addUser( localUserId, @@ -462,14 +464,12 @@ class AuthenticationCubit extends Cubit { final authApi = _apiFactory.createAuthenticationApi(sessionManager.client); + await onPerformLogin?.call(); logger.fd( "Fetching bearer token from the server...", className: runtimeType.toString(), methodName: '_addUser', ); - - await onPerformLogin?.call(); - final token = await authApi.login( username: credentials.username!, password: credentials.password!, @@ -486,7 +486,6 @@ class AuthenticationCubit extends Cubit { clientCertificate: clientCert, authToken: token, ); - final userAccountBox = Hive.box(HiveBoxes.localUserAccount); final userStateBox = @@ -586,12 +585,14 @@ class AuthenticationCubit extends Cubit { clientCertificate: clientCert, ), ); + logger.fd( "User credentials successfully saved.", className: runtimeType.toString(), methodName: '_addUser', ); }); + final hostsBox = Hive.box(HiveBoxes.hosts); if (!hostsBox.values.contains(serverUrl)) { await hostsBox.add(serverUrl); diff --git a/lib/features/login/view/add_account_page.dart b/lib/features/login/view/add_account_page.dart index fcc9aab..7bb82c6 100644 --- a/lib/features/login/view/add_account_page.dart +++ b/lib/features/login/view/add_account_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -17,7 +16,6 @@ import 'package:paperless_mobile/features/login/model/client_certificate_form_mo import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; -import 'package:paperless_mobile/features/login/view/widgets/form_fields/login_settings_page.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; import 'package:paperless_mobile/generated/assets.gen.dart'; @@ -44,6 +42,7 @@ class AddAccountPage extends StatefulWidget { final bool showLocalAccounts; final Widget? bottomLeftButton; + const AddAccountPage({ Key? key, required this.onSubmit, diff --git a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart index 171ab27..ac2d476 100644 --- a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart @@ -5,6 +5,7 @@ import 'package:hive_flutter/adapters.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/keys.dart'; class ServerAddressFormField extends StatefulWidget { static const String fkServerAddress = "serverAddress"; @@ -59,7 +60,7 @@ class _ServerAddressFormFieldState extends State maxWidth: MediaQuery.sizeOf(context).width - 40, ); }, - key: const ValueKey('login-server-address'), + key: TestKeys.login.serverAddressFormField, optionsBuilder: (textEditingValue) { return Hive.box(HiveBoxes.hosts) .values diff --git a/lib/keys.dart b/lib/keys.dart new file mode 100644 index 0000000..43e3197 --- /dev/null +++ b/lib/keys.dart @@ -0,0 +1,19 @@ +import 'package:flutter/widgets.dart'; + +class TestKeys { + TestKeys._(); + + static final login = _LoginTestKeys(); +} + +class _LoginTestKeys { + final serverAddressFormField = const Key('login-server-address'); + final continueButton = const Key('login-continue-button'); + final usernameFormField = const Key('login-username'); + final passwordFormField = const Key('login-password'); + final loginButton = const Key('login-login-button'); + final clientCertificateFormField = const Key('login-client-certificate'); + final clientCertificatePassphraseFormField = + const Key('login-client-certificate-passphrase'); + final loggingInScreen = const Key('login-logging-in-screen'); +} diff --git a/lib/main.dart b/lib/main.dart index c6fb222..55c26fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,6 +24,8 @@ import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/my_bloc_observer.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/hive/hive_extensions.dart'; +import 'package:paperless_mobile/core/database/hive/hive_initialization.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; @@ -31,7 +33,7 @@ import 'package:paperless_mobile/core/exception/server_message_exception.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; -import 'package:paperless_mobile/core/notifier/go_router_refresh_stream.dart'; +import 'package:paperless_mobile/core/security/session_manager_impl.dart'; import 'package:paperless_mobile/features/logging/data/formatted_printer.dart'; import 'package:paperless_mobile/features/logging/data/logger.dart'; import 'package:paperless_mobile/features/logging/data/mirrored_file_output.dart'; @@ -105,65 +107,36 @@ Future performMigrations() async { } } -Future _initHive() async { - await Hive.initFlutter(); - await performMigrations(); - registerHiveAdapters(); - await Hive.openBox(HiveBoxes.localUserAccount); - await Hive.openBox(HiveBoxes.localUserAppState); - await Hive.openBox(HiveBoxes.hintStateBox); - await Hive.openBox(HiveBoxes.hosts); - final globalSettingsBox = - await Hive.openBox(HiveBoxes.globalSettings); +Future initializeDefaultParameters() async { + Bloc.observer = MyBlocObserver(); + await FileService.instance.initialize(); + logger = l.Logger( + output: MirroredFileOutput(), + printer: FormattedPrinter(), + level: l.Level.trace, + filter: l.ProductionFilter(), + ); - if (!globalSettingsBox.hasValue) { - await globalSettingsBox.setValue( - GlobalSettings(preferredLocaleSubtag: defaultPreferredLocale.toString()), - ); + packageInfo = await PackageInfo.fromPlatform(); + + if (Platform.isAndroid) { + androidInfo = await DeviceInfoPlugin().androidInfo; } + if (Platform.isIOS) { + iosInfo = await DeviceInfoPlugin().iosInfo; + } + + await findSystemLocale(); } void main() async { runZonedGuarded(() async { - Bloc.observer = MyBlocObserver(); - WidgetsFlutterBinding.ensureInitialized(); - await FileService.instance.initialize(); - - logger = l.Logger( - output: MirroredFileOutput(), - printer: FormattedPrinter(), - level: l.Level.trace, - filter: l.ProductionFilter(), - ); - Paint.enableDithering = true; - - // if (kDebugMode) { - // // URL: http://localhost:3131 - // // Login: admin:test - // await LocalMockApiServer( - // // RandomDelayGenerator( - // // const Duration(milliseconds: 100), - // // const Duration(milliseconds: 800), - // // ), - // ) - // .start(); - // } - - packageInfo = await PackageInfo.fromPlatform(); - - if (Platform.isAndroid) { - androidInfo = await DeviceInfoPlugin().androidInfo; - } - if (Platform.isIOS) { - iosInfo = await DeviceInfoPlugin().iosInfo; - } - await _initHive(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); - final globalSettingsBox = - Hive.box(HiveBoxes.globalSettings); - final globalSettings = globalSettingsBox.getValue()!; - - await findSystemLocale(); + final hiveDirectory = await getApplicationDocumentsDirectory(); + final defaultLocale = defaultPreferredLocale.languageCode; + await initializeDefaultParameters(); + await initHive(hiveDirectory, defaultLocale); + await performMigrations(); final connectivityStatusService = ConnectivityStatusServiceImpl( Connectivity(), @@ -179,10 +152,10 @@ void main() async { FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); final languageHeaderInterceptor = LanguageHeaderInterceptor( - globalSettings.preferredLocaleSubtag, + () => Hive.globalSettingsBox.getValue()!.preferredLocaleSubtag, ); // Manages security context, required for self signed client certificates - final sessionManager = SessionManager([ + final SessionManager sessionManager = SessionManagerImpl([ PrettyDioLogger( compact: true, responseBody: false, @@ -195,21 +168,9 @@ void main() async { languageHeaderInterceptor, ]); - // Initialize Blocs/Cubits - final connectivityCubit = ConnectivityCubit(connectivityStatusService); - - // Load application settings and stored authentication data - await connectivityCubit.initialize(); - final localNotificationService = LocalNotificationService(); await localNotificationService.initialize(); - //Update language header in interceptor on language change. - globalSettingsBox.listenable().addListener(() { - languageHeaderInterceptor.preferredLocaleSubtag = - globalSettings.preferredLocaleSubtag; - }); - final apiFactory = PaperlessApiFactoryImpl(sessionManager); final authenticationCubit = AuthenticationCubit( localAuthService, @@ -219,33 +180,19 @@ void main() async { localNotificationService, ); runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: sessionManager), - Provider.value(value: localAuthService), - Provider.value( - value: connectivityStatusService), - Provider.value( - value: localNotificationService), - Provider.value(value: DocumentChangedNotifier()), - ], - child: MultiProvider( - providers: [ - Provider.value(value: connectivityCubit), - Provider.value(value: authenticationCubit), - ], - child: GoRouterShell( - apiFactory: apiFactory, - ), - ), + AppEntrypoint( + sessionManager: sessionManager, + apiFactory: apiFactory, + authenticationCubit: authenticationCubit, + connectivityStatusService: connectivityStatusService, + localNotificationService: localNotificationService, + localAuthService: localAuthService, ), ); }, (error, stackTrace) { if (error is StateError && error.message.contains("Cannot emit new states")) { - { - return; - } + return; } // Catches all unexpected/uncaught errors and prints them to the console. final message = switch (error) { @@ -262,9 +209,52 @@ void main() async { }); } +class AppEntrypoint extends StatelessWidget { + final PaperlessApiFactory apiFactory; + final AuthenticationCubit authenticationCubit; + final ConnectivityStatusService connectivityStatusService; + final LocalNotificationService localNotificationService; + final LocalAuthenticationService localAuthService; + final SessionManager sessionManager; + + const AppEntrypoint({ + super.key, + required this.apiFactory, + required this.authenticationCubit, + required this.connectivityStatusService, + required this.localNotificationService, + required this.localAuthService, + required this.sessionManager, + }); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + Provider.value(value: DocumentChangedNotifier()), + Provider.value(value: authenticationCubit), + Provider.value( + value: ConnectivityCubit(connectivityStatusService)..initialize(), + ), + ChangeNotifierProvider.value(value: sessionManager), + Provider.value(value: connectivityStatusService), + Provider.value(value: localNotificationService), + Provider.value(value: localAuthService), + ], + child: GoRouterShell( + apiFactory: apiFactory, + ), + ); + } +} + class GoRouterShell extends StatefulWidget { final PaperlessApiFactory apiFactory; - const GoRouterShell({super.key, required this.apiFactory}); + + const GoRouterShell({ + super.key, + required this.apiFactory, + }); @override State createState() => _GoRouterShellState(); @@ -397,7 +387,7 @@ class _GoRouterShellState extends State { dynamicScheme: darkDynamic, preferredColorScheme: settings.preferredColorSchemeOption, ), - themeMode: settings.preferredThemeMode, + themeMode: settings.preferredThemeMode, supportedLocales: const [ Locale('en'), Locale('de'), diff --git a/lib/routing/routes/login_route.dart b/lib/routing/routes/login_route.dart index 835ba58..96c5547 100644 --- a/lib/routing/routes/login_route.dart +++ b/lib/routing/routes/login_route.dart @@ -12,6 +12,7 @@ import 'package:paperless_mobile/features/login/view/login_to_existing_account_p import 'package:paperless_mobile/features/login/view/verify_identity_page.dart'; import 'package:paperless_mobile/features/login/view/widgets/login_transition_page.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/keys.dart'; import 'package:paperless_mobile/routing/navigation_keys.dart'; import 'package:paperless_mobile/routing/routes.dart'; part 'login_route.g.dart'; @@ -108,6 +109,7 @@ class AuthenticatingRoute extends GoRouteData { }; return NoTransitionPage( child: LoginTransitionPage( + key: TestKeys.login.loggingInScreen, text: text, ), ); diff --git a/lib/theme.dart b/lib/theme.dart index 84771fc..7331415 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -82,14 +82,14 @@ SystemUiOverlayStyle buildOverlayStyle( Brightness.light => SystemUiOverlayStyle.dark.copyWith( systemNavigationBarColor: color, systemNavigationBarDividerColor: color, - // statusBarColor: theme.colorScheme.background, + statusBarColor: theme.colorScheme.background, // statusBarColor: theme.colorScheme.background, // systemNavigationBarDividerColor: theme.colorScheme.surface, ), Brightness.dark => SystemUiOverlayStyle.light.copyWith( systemNavigationBarColor: color, systemNavigationBarDividerColor: color, - // statusBarColor: theme.colorScheme.background, + statusBarColor: theme.colorScheme.background, // statusBarColor: theme.colorScheme.background, // systemNavigationBarDividerColor: theme.colorScheme.surface, ), diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index 9cbc03a..38df520 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -73,7 +73,7 @@ class DocumentModel extends Equatable { this.userCanChange, this.permissions, this.customFields = const [], - this.notes = const [] = const [], + this.notes = const [], }); factory DocumentModel.fromJson(Map json) => diff --git a/packages/paperless_document_scanner/example/pubspec.lock b/packages/paperless_document_scanner/example/pubspec.lock index 433f843..46dcade 100644 --- a/packages/paperless_document_scanner/example/pubspec.lock +++ b/packages/paperless_document_scanner/example/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" colorfilter_generator: dependency: transitive description: @@ -244,10 +244,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" paperless_document_scanner: dependency: "direct main" description: @@ -376,18 +376,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -416,10 +416,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" typed_data: dependency: transitive description: @@ -440,10 +440,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" win32: dependency: transitive description: @@ -469,5 +469,5 @@ packages: source: hosted version: "6.3.0" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.13.0" From ee6a75301b5ed0b8c0b369e1322b43840bff1ecf Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 6 Jan 2024 20:48:19 +0100 Subject: [PATCH 8/8] feat: Bump version number, add changelogs --- .../metadata/android/de-DE/changelogs/4043.txt | 5 +++-- .../metadata/android/en-US/changelogs/4043.txt | 3 +++ build.yaml | 10 +--------- lib/constants.dart | 2 ++ .../changelogs/view/changelog_dialog.dart | 1 + .../login/cubit/authentication_cubit.dart | 16 ++++++++++++---- pubspec.yaml | 2 +- 7 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 android/fastlane/metadata/android/en-US/changelogs/4043.txt diff --git a/android/fastlane/metadata/android/de-DE/changelogs/4043.txt b/android/fastlane/metadata/android/de-DE/changelogs/4043.txt index f591cc4..7a2fa95 100644 --- a/android/fastlane/metadata/android/de-DE/changelogs/4043.txt +++ b/android/fastlane/metadata/android/de-DE/changelogs/4043.txt @@ -1,2 +1,3 @@ -- New feature: Notes -- Several bugfixes +- Neues Feature: Notizen +- Neue Sprache: Italienisch +- Mehere Fehlerbehebungen \ No newline at end of file diff --git a/android/fastlane/metadata/android/en-US/changelogs/4043.txt b/android/fastlane/metadata/android/en-US/changelogs/4043.txt new file mode 100644 index 0000000..da50afe --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/4043.txt @@ -0,0 +1,3 @@ +- New feature: Notes +- New language: Italian +- Multiple bugfixes \ No newline at end of file diff --git a/build.yaml b/build.yaml index 57dfbd7..936a264 100644 --- a/build.yaml +++ b/build.yaml @@ -1,16 +1,8 @@ targets: $default: - include: - - pubspec.yaml - sources: - - assets/** - - lib/$lib$ - - lib/**.dart - - test/**.dart - - integration_test/**.dart - builders: mockito|mockBuilder: generate_for: + - lib/**.dart - test/**.dart - integration_test/**.dart \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 2a86892..cac12c5 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -5,3 +5,5 @@ import 'package:package_info_plus/package_info_plus.dart'; late final PackageInfo packageInfo; late final AndroidDeviceInfo? androidInfo; late final IosDeviceInfo? iosInfo; + +const latestSupportedApiVersion = 3; diff --git a/lib/features/changelogs/view/changelog_dialog.dart b/lib/features/changelogs/view/changelog_dialog.dart index a9804b1..202d271 100644 --- a/lib/features/changelogs/view/changelog_dialog.dart +++ b/lib/features/changelogs/view/changelog_dialog.dart @@ -63,6 +63,7 @@ class ChangelogDialog extends StatelessWidget { } const _versionNumbers = { + "4043": "3.2.0", "4033": "3.1.8", "4023": "3.1.7", "4013": "3.1.6", diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index 3263750..1c1eac6 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/core/bloc/transient_error.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/hive/hive_extensions.dart'; @@ -619,12 +620,19 @@ class AuthenticationCubit extends Cubit { try { final response = await dio.get( "/api/", - options: Options( - sendTimeout: timeout, - ), + options: Options(sendTimeout: timeout), ); - final apiVersion = + int apiVersion = int.parse(response.headers.value('x-api-version') ?? "3"); + if (apiVersion > latestSupportedApiVersion) { + logger.fw( + "The server is running a newer API version ($apiVersion) than the app supports (v$latestSupportedApiVersion), falling back to latest supported version (v$latestSupportedApiVersion). " + "Warning: This might lead to unexpected behavior!", + className: runtimeType.toString(), + methodName: '_getApiVersion', + ); + apiVersion = latestSupportedApiVersion; + } logger.fd( "Successfully retrieved API version ($apiVersion).", className: runtimeType.toString(), diff --git a/pubspec.yaml b/pubspec.yaml index ed1600c..a4ed52c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.1.8+403 +version: 3.2.0+404 environment: sdk: ">=3.1.0 <4.0.0"