From 00b8f317c587a0eba5b6f06922e071ba56161152 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 21 Jan 2023 14:47:18 +0100 Subject: [PATCH 01/25] Ingore workspace files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9e5fafc..34bba21 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ android/key.properties # VS Code which you may wish to be included in version control, so this line # is commented out by default. .vscode/ +*.code-workspace # Flutter/Dart/Pub related **/doc/api/ From a5e19fc0b45c85c2dc4f10a3a390a660a1a827bc Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 21 Jan 2023 22:33:13 +0100 Subject: [PATCH 02/25] Fixed saved view model, slightly changed sorting dialog, fixed open document in system viewer not working because file has folder-like structure. --- .../lib/src/models/document_filter.dart | 11 +++++- .../lib/src/models/document_filter.g.dart | 8 ++-- .../models/query_parameters/sort_field.dart | 2 +- .../lib/src/models/saved_view_model.dart | 39 +++++-------------- .../lib/src/models/saved_view_model.g.dart | 39 +++++++++++++++++++ 5 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 packages/paperless_api/lib/src/models/saved_view_model.g.dart diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index a2b5363..4027ca2 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -26,7 +26,7 @@ class DocumentFilter extends Equatable { final IdQueryParameter storagePath; final IdQueryParameter asnQuery; final TagsQuery tags; - final SortField sortField; + final SortField? sortField; final SortOrder sortOrder; final DateRangeQuery created; final DateRangeQuery added; @@ -59,7 +59,6 @@ class DocumentFilter extends Equatable { List> params = [ MapEntry('page', '$page'), MapEntry('page_size', '$pageSize'), - MapEntry('ordering', '${sortOrder.queryString}${sortField.queryString}'), ...documentType.toQueryParameter('document_type').entries, ...correspondent.toQueryParameter('correspondent').entries, ...storagePath.toQueryParameter('storage_path').entries, @@ -70,6 +69,14 @@ class DocumentFilter extends Equatable { ...modified.toQueryParameter(DateRangeQueryField.modified).entries, ...query.toQueryParameter().entries, ]; + if (sortField != null) { + params.add( + MapEntry( + 'ordering', + '${sortOrder.queryString}${sortField!.queryString}', + ), + ); + } // Reverse ordering can also be encoded using &reverse=1 // Merge query params final queryParams = groupBy(params, (e) => e.key).map( diff --git a/packages/paperless_api/lib/src/models/document_filter.g.dart b/packages/paperless_api/lib/src/models/document_filter.g.dart index 073c517..8c0b89d 100644 --- a/packages/paperless_api/lib/src/models/document_filter.g.dart +++ b/packages/paperless_api/lib/src/models/document_filter.g.dart @@ -59,7 +59,7 @@ Map _$DocumentFilterToJson(DocumentFilter instance) => 'storagePath': instance.storagePath.toJson(), 'asnQuery': instance.asnQuery.toJson(), 'tags': const TagsQueryJsonConverter().toJson(instance.tags), - 'sortField': _$SortFieldEnumMap[instance.sortField]!, + 'sortField': _$SortFieldEnumMap[instance.sortField], 'sortOrder': _$SortOrderEnumMap[instance.sortOrder]!, 'created': const DateRangeQueryJsonConverter().toJson(instance.created), 'added': const DateRangeQueryJsonConverter().toJson(instance.added), @@ -68,10 +68,10 @@ Map _$DocumentFilterToJson(DocumentFilter instance) => }; const _$SortFieldEnumMap = { - SortField.archiveSerialNumber: 'archiveSerialNumber', - SortField.correspondentName: 'correspondentName', + SortField.archiveSerialNumber: 'archive_serial_number', + SortField.correspondentName: 'correspondent__name', SortField.title: 'title', - SortField.documentType: 'documentType', + SortField.documentType: 'document_type__name', SortField.created: 'created', SortField.added: 'added', SortField.modified: 'modified', diff --git a/packages/paperless_api/lib/src/models/query_parameters/sort_field.dart b/packages/paperless_api/lib/src/models/query_parameters/sort_field.dart index cd337b5..7eec450 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/sort_field.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/sort_field.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; -@JsonEnum() +@JsonEnum(valueField: 'queryString') enum SortField { archiveSerialNumber("archive_serial_number"), correspondentName("correspondent__name"), diff --git a/packages/paperless_api/lib/src/models/saved_view_model.dart b/packages/paperless_api/lib/src/models/saved_view_model.dart index 4dfeda0..ae89590 100644 --- a/packages/paperless_api/lib/src/models/saved_view_model.dart +++ b/packages/paperless_api/lib/src/models/saved_view_model.dart @@ -1,9 +1,13 @@ import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/models/document_filter.dart'; import 'package:paperless_api/src/models/filter_rule_model.dart'; import 'package:paperless_api/src/models/query_parameters/sort_field.dart'; import 'package:paperless_api/src/models/query_parameters/sort_order.dart'; +part 'saved_view_model.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake) class SavedView with EquatableMixin { final int? id; final String name; @@ -11,7 +15,7 @@ class SavedView with EquatableMixin { final bool showOnDashboard; final bool showInSidebar; - final SortField sortField; + final SortField? sortField; final bool sortReverse; final List filterRules; @@ -20,7 +24,7 @@ class SavedView with EquatableMixin { required this.name, required this.showOnDashboard, required this.showInSidebar, - required this.sortField, + this.sortField, required this.sortReverse, required this.filterRules, }) { @@ -41,21 +45,10 @@ class SavedView with EquatableMixin { filterRules ]; - SavedView.fromJson(Map json) - : this( - id: json['id'], - name: json['name'], - showOnDashboard: json['show_on_dashboard'], - showInSidebar: json['show_in_sidebar'], - sortField: SortField.values - .where((order) => order.queryString == json['sort_field']) - .first, - sortReverse: json['sort_reverse'], - filterRules: (json['filter_rules'] as List) - .cast>() - .map(FilterRule.fromJson) - .toList(), - ); + factory SavedView.fromJson(Map json) => + _$SavedViewFromJson(json); + + Map toJson() => _$SavedViewToJson(this); DocumentFilter toDocumentFilter() { return filterRules.fold( @@ -81,16 +74,4 @@ class SavedView with EquatableMixin { showOnDashboard: showOnDashboard, sortReverse: filter.sortOrder == SortOrder.descending, ); - - Map toJson() { - return { - 'id': id, - 'name': name, - 'show_on_dashboard': showOnDashboard, - 'show_in_sidebar': showInSidebar, - 'sort_reverse': sortReverse, - 'sort_field': sortField.queryString, - 'filter_rules': filterRules.map((rule) => rule.toJson()).toList(), - }; - } } diff --git a/packages/paperless_api/lib/src/models/saved_view_model.g.dart b/packages/paperless_api/lib/src/models/saved_view_model.g.dart new file mode 100644 index 0000000..012143a --- /dev/null +++ b/packages/paperless_api/lib/src/models/saved_view_model.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'saved_view_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SavedView _$SavedViewFromJson(Map json) => SavedView( + id: json['id'] as int?, + name: json['name'] as String, + showOnDashboard: json['show_on_dashboard'] as bool, + showInSidebar: json['show_in_sidebar'] as bool, + sortField: $enumDecodeNullable(_$SortFieldEnumMap, json['sort_field']), + sortReverse: json['sort_reverse'] as bool, + filterRules: (json['filter_rules'] as List) + .map((e) => FilterRule.fromJson(e as Map)) + .toList(), + ); + +Map _$SavedViewToJson(SavedView instance) => { + 'id': instance.id, + 'name': instance.name, + 'show_on_dashboard': instance.showOnDashboard, + 'show_in_sidebar': instance.showInSidebar, + 'sort_field': _$SortFieldEnumMap[instance.sortField], + 'sort_reverse': instance.sortReverse, + 'filter_rules': instance.filterRules, + }; + +const _$SortFieldEnumMap = { + SortField.archiveSerialNumber: 'archive_serial_number', + SortField.correspondentName: 'correspondent__name', + SortField.title: 'title', + SortField.documentType: 'document_type__name', + SortField.created: 'created', + SortField.added: 'added', + SortField.modified: 'modified', +}; From 1aeebca96b9c7e98b4183148eeef80b5f0995c54 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 21 Jan 2023 22:33:42 +0100 Subject: [PATCH 03/25] Fixed saved view model, slightly changed sorting dialog, fixed open document in system viewer not working because file has folder-like structure. --- .../sort_field_localization_mapper.dart | 22 ++ .../bloc/document_details_cubit.dart | 3 +- .../sort_field_selection_bottom_sheet.dart | 192 +++++++++--------- .../view/widgets/sort_documents_button.dart | 99 +++++---- lib/features/home/view/home_page.dart | 5 +- .../view/widget/bottom_navigation_bar.dart | 64 ------ lib/l10n/intl_cs.arb | 4 + lib/l10n/intl_de.arb | 4 + lib/l10n/intl_en.arb | 4 + lib/l10n/intl_tr.arb | 4 + 10 files changed, 182 insertions(+), 219 deletions(-) create mode 100644 lib/core/translation/sort_field_localization_mapper.dart delete mode 100644 lib/features/home/view/widget/bottom_navigation_bar.dart diff --git a/lib/core/translation/sort_field_localization_mapper.dart b/lib/core/translation/sort_field_localization_mapper.dart new file mode 100644 index 0000000..c2b1f0b --- /dev/null +++ b/lib/core/translation/sort_field_localization_mapper.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +String translateSortField(BuildContext context, SortField sortField) { + switch (sortField) { + case SortField.archiveSerialNumber: + return S.of(context).documentArchiveSerialNumberPropertyShortLabel; + case SortField.correspondentName: + return S.of(context).documentCorrespondentPropertyLabel; + case SortField.title: + return S.of(context).documentTitlePropertyLabel; + case SortField.documentType: + return S.of(context).documentDocumentTypePropertyLabel; + case SortField.created: + return S.of(context).documentCreatedPropertyLabel; + case SortField.added: + return S.of(context).documentAddedPropertyLabel; + case SortField.modified: + return S.of(context).documentModifiedPropertyLabel; + } +} diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart index 7f504bd..71ed062 100644 --- a/lib/features/document_details/bloc/document_details_cubit.dart +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -53,7 +53,8 @@ class DocumentDetailsCubit extends Cubit { final metaData = await _api.getMetaData(state.document); final docBytes = await _api.download(state.document); File f = File('${downloadDir.path}/${metaData.mediaFilename}'); - f.writeAsBytes(docBytes); + f.createSync(recursive: true); + f.writeAsBytesSync(docBytes); return OpenFilex.open(f.path, type: "application/pdf") .then((value) => value.type); } diff --git a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart index 73e6dee..a261cfc 100644 --- a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart +++ b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; @@ -8,9 +10,9 @@ import 'package:paperless_mobile/generated/l10n.dart'; class SortFieldSelectionBottomSheet extends StatefulWidget { final SortOrder initialSortOrder; - final SortField initialSortField; + final SortField? initialSortField; - final Future Function(SortField field, SortOrder order) onSubmit; + final Future Function(SortField? field, SortOrder order) onSubmit; const SortFieldSelectionBottomSheet({ super.key, @@ -26,7 +28,7 @@ class SortFieldSelectionBottomSheet extends StatefulWidget { class _SortFieldSelectionBottomSheetState extends State { - late SortField _currentSortField; + late SortField? _currentSortField; late SortOrder _currentSortOrder; @override @@ -39,61 +41,90 @@ class _SortFieldSelectionBottomSheetState @override Widget build(BuildContext context) { return ClipRRect( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).documentsPageOrderByLabel, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.start, - ), - TextButton( - child: Text(S.of(context).documentFilterApplyFilterLabel), - onPressed: () { - widget.onSubmit( - _currentSortField, - _currentSortOrder, - ); - Navigator.pop(context); - }, - ), - ], - ).paddedSymmetrically(horizontal: 16, vertical: 8.0), - Column( - children: [ - _buildSortOption(SortField.archiveSerialNumber), - BlocBuilder, LabelState>( - builder: (context, state) { - return _buildSortOption( - SortField.correspondentName, - enabled: state.labels.values.fold( - false, - (previousValue, element) => - previousValue || (element.documentCount ?? 0) > 0), - ); - }, - ), - _buildSortOption(SortField.title), - BlocBuilder, LabelState>( - builder: (context, state) { - return _buildSortOption( - SortField.documentType, - enabled: state.labels.values.fold( - false, - (previousValue, element) => - previousValue || (element.documentCount ?? 0) > 0), - ); - }, - ), - _buildSortOption(SortField.created), - _buildSortOption(SortField.added), - _buildSortOption(SortField.modified), - ], - ), - ], + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).documentsPageOrderByLabel, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.start, + ), + TextButton( + child: Text(S.of(context).documentFilterApplyFilterLabel), + onPressed: () { + widget.onSubmit( + _currentSortField, + _currentSortOrder, + ); + Navigator.pop(context); + }, + ), + ], + ).paddedOnly(left: 16, right: 16, top: 8), + Column( + children: [ + _buildSortOption(SortField.archiveSerialNumber), + BlocBuilder, + LabelState>( + builder: (context, state) { + return _buildSortOption( + SortField.correspondentName, + enabled: state.labels.values.fold( + false, + (previousValue, element) => + previousValue || + (element.documentCount ?? 0) > 0), + ); + }, + ), + _buildSortOption(SortField.title), + BlocBuilder, LabelState>( + builder: (context, state) { + return _buildSortOption( + SortField.documentType, + enabled: state.labels.values.fold( + false, + (previousValue, element) => + previousValue || + (element.documentCount ?? 0) > 0), + ); + }, + ), + _buildSortOption(SortField.created), + _buildSortOption(SortField.added), + _buildSortOption(SortField.modified), + const SizedBox(height: 16), + Center( + child: SegmentedButton( + multiSelectionEnabled: false, + showSelectedIcon: false, + segments: [ + ButtonSegment( + icon: const FaIcon(FontAwesomeIcons.arrowDownAZ), + value: SortOrder.descending, + label: Text(S.of(context).sortDocumentDescending), + ), + ButtonSegment( + icon: const FaIcon(FontAwesomeIcons.arrowUpZA), + value: SortOrder.ascending, + label: Text(S.of(context).sortDocumentAscending), + ), + ], + emptySelectionAllowed: false, + selected: {_currentSortOrder}, + onSelectionChanged: (selection) { + setState(() => _currentSortOrder = selection.first); + }, + ), + ).paddedOnly(bottom: 16), + ], + ), + ], + ), ), ); } @@ -101,47 +132,10 @@ class _SortFieldSelectionBottomSheetState Widget _buildSortOption(SortField field, {bool enabled = true}) { return ListTile( enabled: enabled, - contentPadding: const EdgeInsets.symmetric(horizontal: 32), - title: Text( - _localizedSortField(field), - ), - trailing: _currentSortField == field - ? _buildOrderIcon(_currentSortOrder) - : null, - onTap: () { - setState(() { - _currentSortOrder = (_currentSortField == field - ? _currentSortOrder.toggle() - : SortOrder.descending); - _currentSortField = field; - }); - }, + contentPadding: const EdgeInsets.only(left: 32, right: 16), + title: Text(translateSortField(context, field)), + trailing: _currentSortField == field ? const Icon(Icons.done) : null, + onTap: () => setState(() => _currentSortField = field), ); } - - Widget _buildOrderIcon(SortOrder order) { - if (order == SortOrder.ascending) { - return const Icon(Icons.arrow_upward); - } - return const Icon(Icons.arrow_downward); - } - - String _localizedSortField(SortField sortField) { - switch (sortField) { - case SortField.archiveSerialNumber: - return S.of(context).documentArchiveSerialNumberPropertyShortLabel; - case SortField.correspondentName: - return S.of(context).documentCorrespondentPropertyLabel; - case SortField.title: - return S.of(context).documentTitlePropertyLabel; - case SortField.documentType: - return S.of(context).documentDocumentTypePropertyLabel; - case SortField.created: - return S.of(context).documentCreatedPropertyLabel; - case SortField.added: - return S.of(context).documentAddedPropertyLabel; - case SortField.modified: - return S.of(context).documentModifiedPropertyLabel; - } - } } diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index e935e47..cccc276 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -16,60 +16,55 @@ class SortDocumentsButton extends StatelessWidget { Widget build(BuildContext context) { return IconButton( icon: const Icon(Icons.sort), - onPressed: () => _onOpenSortBottomSheet(context), - ); - } - - void _onOpenSortBottomSheet(BuildContext context) { - showModalBottomSheet( - elevation: 2, - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - builder: (_) => BlocProvider.value( - value: context.read(), - child: FractionallySizedBox( - heightFactor: .6, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), - ), - ), - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), - ), - ), - ], - child: BlocBuilder( - builder: (context, state) { - return SortFieldSelectionBottomSheet( - initialSortField: state.filter.sortField, - initialSortOrder: state.filter.sortOrder, - onSubmit: (field, order) => - context.read().updateCurrentFilter( - (filter) => filter.copyWith( - sortField: field, - sortOrder: order, - ), - ), - ); - }, + onPressed: () { + showModalBottomSheet( + elevation: 2, + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), ), ), - ), - ), + builder: (_) => BlocProvider.value( + value: context.read(), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), + ), + ), + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), + ), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return SortFieldSelectionBottomSheet( + initialSortField: state.filter.sortField, + initialSortOrder: state.filter.sortOrder, + onSubmit: (field, order) => + context.read().updateCurrentFilter( + (filter) => filter.copyWith( + sortField: field, + sortOrder: order, + ), + ), + ); + }, + ), + ), + ), + ); + }, ); } } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 4ec53af..ac8ce5a 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -8,19 +8,18 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/global/constants.dart'; -import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; +import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/route_description.dart'; -import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart'; import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; @@ -31,8 +30,8 @@ import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; -import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:path/path.dart' as p; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:responsive_builder/responsive_builder.dart'; class HomePage extends StatefulWidget { diff --git a/lib/features/home/view/widget/bottom_navigation_bar.dart b/lib/features/home/view/widget/bottom_navigation_bar.dart deleted file mode 100644 index 2b9a27a..0000000 --- a/lib/features/home/view/widget/bottom_navigation_bar.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; - -class BottomNavBar extends StatelessWidget { - final int selectedIndex; - final void Function(int) onNavigationChanged; - - const BottomNavBar( - {Key? key, - required this.selectedIndex, - required this.onNavigationChanged}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return NavigationBar( - elevation: 4.0, - onDestinationSelected: onNavigationChanged, - selectedIndex: selectedIndex, - destinations: [ - NavigationDestination( - icon: const Icon(Icons.description_outlined), - selectedIcon: Icon( - Icons.description, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context).bottomNavDocumentsPageLabel, - ), - NavigationDestination( - icon: const Icon(Icons.document_scanner_outlined), - selectedIcon: Icon( - Icons.document_scanner, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context).bottomNavScannerPageLabel, - ), - NavigationDestination( - icon: const Icon(Icons.sell_outlined), - selectedIcon: Icon( - Icons.sell, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context).bottomNavLabelsPageLabel, - ), - NavigationDestination( - icon: const Icon(Icons.inbox_outlined), - selectedIcon: Icon( - Icons.inbox, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context).bottomNavInboxPageLabel, - ), - NavigationDestination( - icon: const Icon(Icons.settings_outlined), - selectedIcon: Icon( - Icons.settings, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context).appDrawerSettingsLabel, - ), - ], - ); - } -} diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 204cc91..76222dc 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -584,6 +584,10 @@ "@settingsThemeModeLightLabel": {}, "settingsThemeModeSystemLabel": "Systémový", "@settingsThemeModeSystemLabel": {}, + "sortDocumentAscending": "Ascending", + "@sortDocumentAscending": {}, + "sortDocumentDescending": "Descending", + "@sortDocumentDescending": {}, "storagePathParameterDayLabel": "den", "@storagePathParameterDayLabel": {}, "storagePathParameterMonthLabel": "měsíc", diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index d2ddc85..4738b2d 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -584,6 +584,10 @@ "@settingsThemeModeLightLabel": {}, "settingsThemeModeSystemLabel": "System", "@settingsThemeModeSystemLabel": {}, + "sortDocumentAscending": "Aufsteigend", + "@sortDocumentAscending": {}, + "sortDocumentDescending": "Absteigend", + "@sortDocumentDescending": {}, "storagePathParameterDayLabel": "Tag", "@storagePathParameterDayLabel": {}, "storagePathParameterMonthLabel": "Monat", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1ba5eed..5243d9d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -584,6 +584,10 @@ "@settingsThemeModeLightLabel": {}, "settingsThemeModeSystemLabel": "System", "@settingsThemeModeSystemLabel": {}, + "sortDocumentAscending": "Ascending", + "@sortDocumentAscending": {}, + "sortDocumentDescending": "Descending", + "@sortDocumentDescending": {}, "storagePathParameterDayLabel": "day", "@storagePathParameterDayLabel": {}, "storagePathParameterMonthLabel": "month", diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 17651e8..17f7ae1 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -584,6 +584,10 @@ "@settingsThemeModeLightLabel": {}, "settingsThemeModeSystemLabel": "Sistem", "@settingsThemeModeSystemLabel": {}, + "sortDocumentAscending": "Ascending", + "@sortDocumentAscending": {}, + "sortDocumentDescending": "Descending", + "@sortDocumentDescending": {}, "storagePathParameterDayLabel": "gün", "@storagePathParameterDayLabel": {}, "storagePathParameterMonthLabel": "ay", From d4978172cf83ebc7bd426d0b3d1a3f7b9f699b8d Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 21 Jan 2023 22:45:12 +0100 Subject: [PATCH 04/25] Bump version to 1.5.3 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index eb18cd7..899daae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,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: 1.5.2+15 +version: 1.5.3+16 environment: sdk: '>=3.0.0-35.0.dev <4.0.0' From b370fa416470b0de2ca13218e60dfc1233c1d967 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sun, 22 Jan 2023 01:17:52 +0100 Subject: [PATCH 05/25] WIP - Implemented similar documents view --- .../documents_list_loading_widget.dart | 151 +++++----- lib/core/widgets/hint_card.dart | 8 +- .../view/pages/document_details_page.dart | 274 +++++++++--------- .../view/pages/similar_documents_view.dart | 94 ++++++ .../documents/view/pages/documents_page.dart | 18 +- .../view/widgets/documents_empty_state.dart | 5 +- .../widgets/list/adaptive_documents_view.dart | 7 - .../view/widgets/list/document_list_item.dart | 16 +- .../view/pages/linked_documents_page.dart | 6 +- .../model/documents_paged_state.dart | 4 + .../cubit/similar_documents_cubit.dart | 28 ++ .../cubit/similar_documents_state.dart | 47 +++ lib/l10n/intl_cs.arb | 2 + lib/l10n/intl_de.arb | 2 + lib/l10n/intl_en.arb | 2 + lib/l10n/intl_tr.arb | 2 + .../lib/src/converters/converters.dart | 1 - ...similar_document_model_json_converter.dart | 15 - .../lib/src/models/document_filter.dart | 11 +- .../lib/src/models/document_filter.g.dart | 2 + .../lib/src/models/document_model.dart | 8 + .../lib/src/models/document_model.g.dart | 45 ++- .../paperless_api/lib/src/models/models.dart | 1 - .../src/models/similar_document_model.dart | 36 --- .../src/models/similar_document_model.g.dart | 50 ---- .../paperless_documents_api.dart | 1 - .../paperless_documents_api_impl.dart | 21 -- 27 files changed, 476 insertions(+), 381 deletions(-) create mode 100644 lib/features/document_details/view/pages/similar_documents_view.dart create mode 100644 lib/features/similar_documents/cubit/similar_documents_cubit.dart create mode 100644 lib/features/similar_documents/cubit/similar_documents_state.dart delete mode 100644 packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart delete mode 100644 packages/paperless_api/lib/src/models/similar_document_model.dart delete mode 100644 packages/paperless_api/lib/src/models/similar_document_model.g.dart diff --git a/lib/core/widgets/documents_list_loading_widget.dart b/lib/core/widgets/documents_list_loading_widget.dart index 6f0f920..8d1575c 100644 --- a/lib/core/widgets/documents_list_loading_widget.dart +++ b/lib/core/widgets/documents_list_loading_widget.dart @@ -5,84 +5,95 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:shimmer/shimmer.dart'; class DocumentsListLoadingWidget extends StatelessWidget { - final List above; - final List below; - static const tags = [" ", " ", " "]; - static const titleLengths = [double.infinity, 150.0, 200.0]; - static const correspondentLengths = [200.0, 300.0, 150.0]; - static const fontSize = 16.0; + final List beforeWidgets; + final List afterWidgets; + + static const _tags = [" ", " ", " "]; + static const _titleLengths = [double.infinity, 150.0, 200.0]; + static const _correspondentLengths = [200.0, 300.0, 150.0]; + static const _fontSize = 16.0; const DocumentsListLoadingWidget({ super.key, - this.above = const [], - this.below = const [], + this.beforeWidgets = const [], + this.afterWidgets = const [], }); @override Widget build(BuildContext context) { - return ListView( - children: [ - ...above, - ...List.generate(25, (idx) { - final r = Random(idx); - final tagCount = r.nextInt(tags.length + 1); - final correspondentLength = - correspondentLengths[r.nextInt(correspondentLengths.length - 1)]; - final titleLength = titleLengths[r.nextInt(titleLengths.length - 1)]; - return Shimmer.fromColors( - baseColor: Theme.of(context).brightness == Brightness.light - ? Colors.grey[300]! - : Colors.grey[900]!, - highlightColor: Theme.of(context).brightness == Brightness.light - ? Colors.grey[100]! - : Colors.grey[600]!, - child: ListTile( - contentPadding: const EdgeInsets.all(8), - dense: true, - isThreeLine: true, - leading: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( - color: Colors.white, - height: 50, - width: 35, - ), - ), - title: Container( - padding: const EdgeInsets.symmetric(vertical: 2.0), - width: correspondentLength, - height: fontSize, - color: Colors.white, - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 2.0), - height: fontSize, - width: titleLength, - color: Colors.white, - ), - Wrap( - spacing: 2.0, - children: List.generate( - tagCount, - (index) => InputChip( - label: Text(tags[r.nextInt(tags.length)]), - ), - ), - ).paddedOnly(top: 4), - ], - ), - ), - ), - ); - }).toList(), - ...below, + final _random = Random(); + return CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate(beforeWidgets), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return _buildFakeListItem(context, _random); + }, + ), + ), + SliverList(delegate: SliverChildListDelegate(afterWidgets)) ], ); } + + Widget _buildFakeListItem(BuildContext context, Random random) { + final tagCount = random.nextInt(_tags.length + 1); + final correspondentLength = + _correspondentLengths[random.nextInt(_correspondentLengths.length - 1)]; + final titleLength = _titleLengths[random.nextInt(_titleLengths.length - 1)]; + return Shimmer.fromColors( + baseColor: Theme.of(context).brightness == Brightness.light + ? Colors.grey[300]! + : Colors.grey[900]!, + highlightColor: Theme.of(context).brightness == Brightness.light + ? Colors.grey[100]! + : Colors.grey[600]!, + child: ListTile( + contentPadding: const EdgeInsets.all(8), + dense: true, + isThreeLine: true, + leading: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + color: Colors.white, + height: 50, + width: 35, + ), + ), + title: Container( + padding: const EdgeInsets.symmetric(vertical: 2.0), + width: correspondentLength, + height: _fontSize, + color: Colors.white, + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 2.0), + height: _fontSize, + width: titleLength, + color: Colors.white, + ), + Wrap( + spacing: 2.0, + children: List.generate( + tagCount, + (index) => InputChip( + label: Text(_tags[random.nextInt(_tags.length)]), + ), + ), + ).paddedOnly(top: 4), + ], + ), + ), + ), + ); + } } diff --git a/lib/core/widgets/hint_card.dart b/lib/core/widgets/hint_card.dart index 27b6ddb..d32ff04 100644 --- a/lib/core/widgets/hint_card.dart +++ b/lib/core/widgets/hint_card.dart @@ -6,6 +6,7 @@ import 'package:paperless_mobile/generated/l10n.dart'; class HintCard extends StatelessWidget { final String hintText; final double elevation; + final IconData hintIcon; final VoidCallback? onHintAcknowledged; final bool show; const HintCard({ @@ -13,7 +14,8 @@ class HintCard extends StatelessWidget { required this.hintText, this.onHintAcknowledged, this.elevation = 1, - required this.show, + this.show = true, + this.hintIcon = Icons.tips_and_updates_outlined, }); @override @@ -31,7 +33,7 @@ class HintCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( - Icons.tips_and_updates_outlined, + hintIcon, color: Theme.of(context).hintColor, ).padded(), Align( @@ -52,7 +54,7 @@ class HintCard extends StatelessWidget { ), ) else - Padding(padding: EdgeInsets.only(bottom: 24)), + const Padding(padding: EdgeInsets.only(bottom: 24)), ], ).padded(), ).padded(), 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 17270e5..d8d7535 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -14,6 +14,7 @@ import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/similar_documents_view.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; @@ -23,6 +24,7 @@ import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubi import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; +import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:path_provider/path_provider.dart'; @@ -31,6 +33,7 @@ import 'package:badges/badges.dart' as b; import '../../../../core/repository/state/impl/document_type_repository_state.dart'; +//TODO: Refactor this into several widgets class DocumentDetailsPage extends StatefulWidget { final bool allowEdit; final bool isLabelClickable; @@ -48,6 +51,16 @@ class DocumentDetailsPage extends StatefulWidget { } class _DocumentDetailsPageState extends State { + late Future _metaData; + + @override + void initState() { + super.initState(); + _metaData = context + .read() + .getMetaData(context.read().state.document); + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -57,102 +70,11 @@ class _DocumentDetailsPageState extends State { return false; }, child: DefaultTabController( - length: 3, + length: 4, child: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, - floatingActionButton: widget.allowEdit - ? BlocBuilder( - builder: (context, state) { - final _filteredSuggestions = - state.suggestions.documentDifference(state.document); - return BlocBuilder( - builder: (context, connectivityState) { - if (!connectivityState.isConnected) { - return Container(); - } - return b.Badge( - position: b.BadgePosition.topEnd(top: -12, end: -6), - showBadge: _filteredSuggestions.hasSuggestions, - child: Tooltip( - message: - S.of(context).documentDetailsPageEditTooltip, - preferBelow: false, - verticalOffset: 40, - child: FloatingActionButton( - child: const Icon(Icons.edit), - onPressed: () => _onEdit(state.document), - ), - ), - badgeContent: Text( - '${_filteredSuggestions.suggestionsCount}', - style: const TextStyle( - color: Colors.white, - ), - ), - badgeColor: Colors.red, - ); - }, - ); - }, - ) - : null, - bottomNavigationBar: - BlocBuilder( - builder: (context, state) { - return BottomAppBar( - child: BlocBuilder( - builder: (context, connectivityState) { - final isConnected = connectivityState.isConnected; - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - tooltip: - S.of(context).documentDetailsPageDeleteTooltip, - icon: const Icon(Icons.delete), - onPressed: widget.allowEdit && isConnected - ? () => _onDelete(state.document) - : null, - ).paddedSymmetrically(horizontal: 4), - Tooltip( - message: - S.of(context).documentDetailsPageDownloadTooltip, - child: DocumentDownloadButton( - document: state.document, - enabled: isConnected, - ), - ), - IconButton( - tooltip: - S.of(context).documentDetailsPagePreviewTooltip, - icon: const Icon(Icons.visibility), - onPressed: isConnected - ? () => _onOpen(state.document) - : null, - ).paddedOnly(right: 4.0), - IconButton( - tooltip: S - .of(context) - .documentDetailsPageOpenInSystemViewerTooltip, - icon: const Icon(Icons.open_in_new), - onPressed: - isConnected ? _onOpenFileInSystemViewer : null, - ).paddedOnly(right: 4.0), - IconButton( - tooltip: - S.of(context).documentDetailsPageShareTooltip, - icon: const Icon(Icons.share), - onPressed: isConnected - ? () => _onShare(state.document) - : null, - ), - ], - ); - }, - ), - ); - }, - ), + floatingActionButton: widget.allowEdit ? _buildAppBar() : null, + bottomNavigationBar: _buildBottomAppBar(), body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverAppBar( @@ -180,6 +102,7 @@ class _DocumentDetailsPageState extends State { backgroundColor: Theme.of(context).colorScheme.primaryContainer, tabBar: TabBar( + isScrollable: true, tabs: [ Tab( child: Text( @@ -208,6 +131,18 @@ class _DocumentDetailsPageState extends State { .onPrimaryContainer), ), ), + Tab( + child: Text( + S + .of(context) + .documentDetailsPageTabSimilarDocumentsLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), ], ), ), @@ -215,19 +150,26 @@ class _DocumentDetailsPageState extends State { ], body: BlocBuilder( builder: (context, state) { - return TabBarView( - children: [ - _buildDocumentOverview( - state.document, - ), - _buildDocumentContentView( - state.document, - state, - ), - _buildDocumentMetaDataView( - state.document, - ), - ], + return BlocProvider( + create: (context) => SimilarDocumentsCubit( + context.read(), + documentId: state.document.id, + ), + child: TabBarView( + children: [ + _buildDocumentOverview( + state.document, + ), + _buildDocumentContentView( + state.document, + state, + ), + _buildDocumentMetaDataView( + state.document, + ), + _buildSimilarDocumentsView(), + ], + ), ).paddedSymmetrically(horizontal: 8); }, ), @@ -237,6 +179,94 @@ class _DocumentDetailsPageState extends State { ); } + BlocBuilder _buildAppBar() { + return BlocBuilder( + builder: (context, state) { + final _filteredSuggestions = + state.suggestions.documentDifference(state.document); + return BlocBuilder( + builder: (context, connectivityState) { + if (!connectivityState.isConnected) { + return Container(); + } + return b.Badge( + position: b.BadgePosition.topEnd(top: -12, end: -6), + showBadge: _filteredSuggestions.hasSuggestions, + child: Tooltip( + message: S.of(context).documentDetailsPageEditTooltip, + preferBelow: false, + verticalOffset: 40, + child: FloatingActionButton( + child: const Icon(Icons.edit), + onPressed: () => _onEdit(state.document), + ), + ), + badgeContent: Text( + '${_filteredSuggestions.suggestionsCount}', + style: const TextStyle( + color: Colors.white, + ), + ), + badgeColor: Colors.red, + ); + }, + ); + }, + ); + } + + BlocBuilder _buildBottomAppBar() { + return BlocBuilder( + builder: (context, state) { + return BottomAppBar( + child: BlocBuilder( + builder: (context, connectivityState) { + final isConnected = connectivityState.isConnected; + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + tooltip: S.of(context).documentDetailsPageDeleteTooltip, + icon: const Icon(Icons.delete), + onPressed: widget.allowEdit && isConnected + ? () => _onDelete(state.document) + : null, + ).paddedSymmetrically(horizontal: 4), + Tooltip( + message: S.of(context).documentDetailsPageDownloadTooltip, + child: DocumentDownloadButton( + document: state.document, + enabled: isConnected, + ), + ), + IconButton( + tooltip: S.of(context).documentDetailsPagePreviewTooltip, + icon: const Icon(Icons.visibility), + onPressed: + isConnected ? () => _onOpen(state.document) : null, + ).paddedOnly(right: 4.0), + IconButton( + tooltip: S + .of(context) + .documentDetailsPageOpenInSystemViewerTooltip, + icon: const Icon(Icons.open_in_new), + onPressed: isConnected ? _onOpenFileInSystemViewer : null, + ).paddedOnly(right: 4.0), + IconButton( + tooltip: S.of(context).documentDetailsPageShareTooltip, + icon: const Icon(Icons.share), + onPressed: + isConnected ? () => _onShare(state.document) : null, + ), + ], + ); + }, + ), + ); + }, + ); + } + Future _onEdit(DocumentModel document) async { { final cubit = context.read(); @@ -306,7 +336,7 @@ class _DocumentDetailsPageState extends State { ); } return FutureBuilder( - future: context.read().getMetaData(document), + future: _metaData, builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); @@ -465,34 +495,10 @@ class _DocumentDetailsPageState extends State { child: TagsWidget( isClickable: widget.isLabelClickable, tagIds: document.tags, - onTagSelected: (int tagId) {}, ), ), ).paddedSymmetrically(vertical: 16), ), - // _separator(), - // FutureBuilder>( - // future: getIt().findSimilar(document.id), - // builder: (context, snapshot) { - // if (!snapshot.hasData) { - // return CircularProgressIndicator(); - // } - // return ExpansionTile( - // tilePadding: const EdgeInsets.symmetric(horizontal: 8.0), - // title: Text( - // S.of(context).documentDetailsPageSimilarDocumentsLabel, - // style: - // Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold), - // ), - // children: snapshot.data! - // .map((e) => DocumentListItem( - // document: e, - // onTap: (doc) {}, - // isSelected: false, - // isAtLeastOneSelected: false)) - // .toList(), - // ); - // }), ], ); } @@ -558,6 +564,10 @@ class _DocumentDetailsPageState extends State { ' ' + suffixes[i]; } + + Widget _buildSimilarDocumentsView() { + return const SimilarDocumentsView(); + } } class _DetailsItem extends StatelessWidget { diff --git a/lib/features/document_details/view/pages/similar_documents_view.dart b/lib/features/document_details/view/pages/similar_documents_view.dart new file mode 100644 index 0000000..70e7560 --- /dev/null +++ b/lib/features/document_details/view/pages/similar_documents_view.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/core/widgets/hint_card.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; +import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; +import 'package:paperless_mobile/util.dart'; + +class SimilarDocumentsView extends StatefulWidget { + const SimilarDocumentsView({super.key}); + + @override + State createState() => _SimilarDocumentsViewState(); +} + +class _SimilarDocumentsViewState extends State { + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_listenForLoadNewData); + try { + context.read().initialize(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + + @override + void dispose() { + _scrollController.removeListener(_listenForLoadNewData); + super.dispose(); + } + + void _listenForLoadNewData() async { + final currState = context.read().state; + if (_scrollController.offset >= + _scrollController.position.maxScrollExtent * 0.75 && + !currState.isLoading && + !currState.isLastPageLoaded) { + try { + await context.read().loadMore(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + } + + @override + Widget build(BuildContext context) { + const earlyPreviewHintCard = HintCard( + hintIcon: Icons.construction, + hintText: "This view is still work in progress.", + ); + return BlocBuilder( + builder: (context, state) { + if (!state.hasLoaded) { + return const DocumentsListLoadingWidget( + beforeWidgets: [earlyPreviewHintCard], + ); + } + if (state.documents.isEmpty) { + return DocumentsEmptyState( + state: state, + onReset: () => context.read().updateFilter( + filter: DocumentFilter.initial.copyWith( + moreLike: () => + context.read().documentId, + ), + ), + ); + } + return CustomScrollView( + controller: _scrollController, + slivers: [ + const SliverToBoxAdapter(child: earlyPreviewHintCard), + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: state.documents.length, + (context, index) => DocumentListItem( + document: state.documents[index], + enableHeroAnimation: false, + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 088a5bf..6306c0e 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -66,13 +66,17 @@ class _DocumentsPageState extends State { ..addListener(_listenForLoadNewData); } - void _listenForLoadNewData() { + void _listenForLoadNewData() async { final currState = context.read().state; if (_scrollController.offset >= _scrollController.position.maxScrollExtent * 0.75 && !currState.isLoading && !currState.isLastPageLoaded) { - _loadNewPage(); + try { + await context.read().loadMore(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } } } @@ -353,8 +357,6 @@ class _DocumentsPageState extends State { .equals(previous.documents, current.documents) || previous.selectedIds != current.selectedIds, builder: (context, state) { - // Some ugly tricks to make it work with bloc, update pageController - if (state.hasLoaded && state.documents.isEmpty) { return DocumentsEmptyState( state: state, @@ -491,14 +493,6 @@ class _DocumentsPageState extends State { } } - Future _loadNewPage() async { - try { - await context.read().loadMore(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - void _onSelected(DocumentModel model) { context.read().toggleDocumentSelection(model); } diff --git a/lib/features/documents/view/widgets/documents_empty_state.dart b/lib/features/documents/view/widgets/documents_empty_state.dart index 050507c..0d5e6cd 100644 --- a/lib/features/documents/view/widgets/documents_empty_state.dart +++ b/lib/features/documents/view/widgets/documents_empty_state.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/empty_state.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class DocumentsEmptyState extends StatelessWidget { - final DocumentsState state; + final DocumentsPagedState state; final VoidCallback onReset; const DocumentsEmptyState({ Key? key, diff --git a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart index f7d3267..57ba0f0 100644 --- a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart @@ -64,13 +64,6 @@ class AdaptiveDocumentsView extends StatelessWidget { isSelected: state.selectedIds.contains(document.id), onSelected: onSelected, isAtLeastOneSelected: state.selection.isNotEmpty, - isTagSelectedPredicate: (int tagId) { - return state.filter.tags is IdsTagsQuery - ? (state.filter.tags as IdsTagsQuery) - .includedIds - .contains(tagId) - : false; - }, onTagSelected: onTagSelected, onCorrespondentSelected: onCorrespondentSelected, onDocumentTypeSelected: onDocumentTypeSelected, diff --git a/lib/features/documents/view/widgets/list/document_list_item.dart b/lib/features/documents/view/widgets/list/document_list_item.dart index 0c0490b..116bf63 100644 --- a/lib/features/documents/view/widgets/list/document_list_item.dart +++ b/lib/features/documents/view/widgets/list/document_list_item.dart @@ -7,31 +7,32 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d class DocumentListItem extends StatelessWidget { static const _a4AspectRatio = 1 / 1.4142; final DocumentModel document; - final void Function(DocumentModel) onTap; + final void Function(DocumentModel)? onTap; final void Function(DocumentModel)? onSelected; final bool isSelected; final bool isAtLeastOneSelected; final bool isLabelClickable; - final bool Function(int tagId) isTagSelectedPredicate; final void Function(int tagId)? onTagSelected; final void Function(int? correspondentId)? onCorrespondentSelected; final void Function(int? documentTypeId)? onDocumentTypeSelected; final void Function(int? id)? onStoragePathSelected; + final bool enableHeroAnimation; + const DocumentListItem({ Key? key, required this.document, - required this.onTap, + this.onTap, this.onSelected, - required this.isSelected, - required this.isAtLeastOneSelected, + this.isSelected = false, + this.isAtLeastOneSelected = false, this.isLabelClickable = true, - required this.isTagSelectedPredicate, this.onTagSelected, this.onCorrespondentSelected, this.onDocumentTypeSelected, this.onStoragePathSelected, + this.enableHeroAnimation = true, }) : super(key: key); @override @@ -85,6 +86,7 @@ class DocumentListItem extends StatelessWidget { id: document.id, fit: BoxFit.cover, alignment: Alignment.topCenter, + enableHero: enableHeroAnimation, ), ), ), @@ -96,7 +98,7 @@ class DocumentListItem extends StatelessWidget { if (isAtLeastOneSelected || isSelected) { onSelected?.call(document); } else { - onTap(document); + onTap?.call(document); } } } diff --git a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart index bdba0c6..fa39d38 100644 --- a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart @@ -33,7 +33,7 @@ class _LinkedDocumentsPageState extends State { style: Theme.of(context).textTheme.bodySmall, ), if (!state.isLoaded) - Expanded(child: const DocumentsListLoadingWidget()) + const Expanded(child: DocumentsListLoadingWidget()) else Expanded( child: ListView.builder( @@ -59,10 +59,6 @@ class _LinkedDocumentsPageState extends State { ), ); }, - isSelected: false, - isAtLeastOneSelected: false, - isTagSelectedPredicate: (_) => false, - onTagSelected: (int tag) {}, ); }, ), diff --git a/lib/features/paged_document_view/model/documents_paged_state.dart b/lib/features/paged_document_view/model/documents_paged_state.dart index dd9920c..71df68b 100644 --- a/lib/features/paged_document_view/model/documents_paged_state.dart +++ b/lib/features/paged_document_view/model/documents_paged_state.dart @@ -1,6 +1,10 @@ import 'package:equatable/equatable.dart'; import 'package:paperless_api/paperless_api.dart'; +/// +/// Base state for all blocs/cubits using a paged view of documents. +/// [T] is the return type of the API call. +/// abstract class DocumentsPagedState extends Equatable { final bool hasLoaded; final bool isLoading; diff --git a/lib/features/similar_documents/cubit/similar_documents_cubit.dart b/lib/features/similar_documents/cubit/similar_documents_cubit.dart new file mode 100644 index 0000000..13dd481 --- /dev/null +++ b/lib/features/similar_documents/cubit/similar_documents_cubit.dart @@ -0,0 +1,28 @@ +import 'package:bloc/bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; + +part 'similar_documents_state.dart'; + +class SimilarDocumentsCubit extends Cubit + with DocumentsPagingMixin { + final int documentId; + + @override + final PaperlessDocumentsApi api; + + SimilarDocumentsCubit( + this.api, { + required this.documentId, + }) : super(const SimilarDocumentsState()); + + Future initialize() async { + if (!state.hasLoaded) { + await updateFilter( + filter: state.filter.copyWith(moreLike: () => documentId), + ); + emit(state.copyWith(hasLoaded: true)); + } + } +} diff --git a/lib/features/similar_documents/cubit/similar_documents_state.dart b/lib/features/similar_documents/cubit/similar_documents_state.dart new file mode 100644 index 0000000..4c4c664 --- /dev/null +++ b/lib/features/similar_documents/cubit/similar_documents_state.dart @@ -0,0 +1,47 @@ +part of 'similar_documents_cubit.dart'; + +class SimilarDocumentsState extends DocumentsPagedState { + const SimilarDocumentsState({ + super.filter, + super.hasLoaded, + super.isLoading, + super.value, + }); + + @override + List get props => [ + filter, + hasLoaded, + isLoading, + value, + ]; + + @override + SimilarDocumentsState copyWithPaged({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return copyWith( + hasLoaded: hasLoaded, + isLoading: isLoading, + value: value, + filter: filter, + ); + } + + SimilarDocumentsState copyWith({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return SimilarDocumentsState( + hasLoaded: hasLoaded ?? this.hasLoaded, + isLoading: isLoading ?? this.isLoading, + value: value ?? this.value, + filter: filter ?? this.filter, + ); + } +} diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 76222dc..b425bbc 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -90,6 +90,8 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Přehled", "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Typ dokumentu", "@documentDocumentTypePropertyLabel": {}, "documentDownloadSuccessMessage": "Dokument úspěšně stažen.", diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 4738b2d..02b9924 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -90,6 +90,8 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Übersicht", "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Ähnliche Dokumente", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Dokumenttyp", "@documentDocumentTypePropertyLabel": {}, "documentDownloadSuccessMessage": "Dokument erfolgreich heruntergeladen.", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5243d9d..72a3ce5 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -90,6 +90,8 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Overview", "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Document Type", "@documentDocumentTypePropertyLabel": {}, "documentDownloadSuccessMessage": "Document successfully downloaded.", diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 17f7ae1..61dd7c7 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -90,6 +90,8 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Genel bakış", "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Döküman tipi", "@documentDocumentTypePropertyLabel": {}, "documentDownloadSuccessMessage": "Döküman başarıyla indirildi.", diff --git a/packages/paperless_api/lib/src/converters/converters.dart b/packages/paperless_api/lib/src/converters/converters.dart index 10a8c8e..8a96d0c 100644 --- a/packages/paperless_api/lib/src/converters/converters.dart +++ b/packages/paperless_api/lib/src/converters/converters.dart @@ -1,3 +1,2 @@ export 'document_model_json_converter.dart'; -export 'similar_document_model_json_converter.dart'; export 'date_range_query_json_converter.dart'; diff --git a/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart b/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart deleted file mode 100644 index 2b34c84..0000000 --- a/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/paperless_api.dart'; - -class SimilarDocumentModelJsonConverter - extends JsonConverter> { - @override - SimilarDocumentModel fromJson(Map json) { - return SimilarDocumentModel.fromJson(json); - } - - @override - Map toJson(SimilarDocumentModel object) { - return object.toJson(); - } -} diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index 4027ca2..f612cdd 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -33,6 +33,9 @@ class DocumentFilter extends Equatable { final DateRangeQuery modified; final TextQuery query; + /// Query documents similar to the document with this id. + final int? moreLike; + const DocumentFilter({ this.documentType = const IdQueryParameter.unset(), this.correspondent = const IdQueryParameter.unset(), @@ -47,6 +50,7 @@ class DocumentFilter extends Equatable { this.added = const UnsetDateRangeQuery(), this.created = const UnsetDateRangeQuery(), this.modified = const UnsetDateRangeQuery(), + this.moreLike, }); bool get forceExtendedQuery { @@ -77,6 +81,10 @@ class DocumentFilter extends Equatable { ), ); } + + if (moreLike != null) { + params.add(MapEntry('more_like_id', moreLike.toString())); + } // Reverse ordering can also be encoded using &reverse=1 // Merge query params final queryParams = groupBy(params, (e) => e.key).map( @@ -107,7 +115,7 @@ class DocumentFilter extends Equatable { DateRangeQuery? created, DateRangeQuery? modified, TextQuery? query, - int? selectedViewId, + int? Function()? moreLike, }) { final newFilter = DocumentFilter( pageSize: pageSize ?? this.pageSize, @@ -123,6 +131,7 @@ class DocumentFilter extends Equatable { added: added ?? this.added, created: created ?? this.created, modified: modified ?? this.modified, + moreLike: moreLike != null ? moreLike.call() : this.moreLike, ); if (query?.queryType != QueryType.extended && newFilter.forceExtendedQuery) { diff --git a/packages/paperless_api/lib/src/models/document_filter.g.dart b/packages/paperless_api/lib/src/models/document_filter.g.dart index 8c0b89d..6885d77 100644 --- a/packages/paperless_api/lib/src/models/document_filter.g.dart +++ b/packages/paperless_api/lib/src/models/document_filter.g.dart @@ -48,6 +48,7 @@ DocumentFilter _$DocumentFilterFromJson(Map json) => ? const UnsetDateRangeQuery() : const DateRangeQueryJsonConverter() .fromJson(json['modified'] as Map), + moreLike: json['moreLike'] as int?, ); Map _$DocumentFilterToJson(DocumentFilter instance) => @@ -65,6 +66,7 @@ Map _$DocumentFilterToJson(DocumentFilter instance) => 'added': const DateRangeQueryJsonConverter().toJson(instance.added), 'modified': const DateRangeQueryJsonConverter().toJson(instance.modified), 'query': instance.query.toJson(), + 'moreLike': instance.moreLike, }; const _$SortFieldEnumMap = { diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index 9fad865..2c547c5 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; +import 'package:paperless_api/src/models/search_hit.dart'; part 'document_model.g.dart'; @@ -37,6 +38,12 @@ class DocumentModel extends Equatable { final String originalFileName; final String? archivedFileName; + @JsonKey( + name: '__search_hit__', + includeIfNull: false, + ) + final SearchHit? searchHit; + const DocumentModel({ required this.id, required this.title, @@ -51,6 +58,7 @@ class DocumentModel extends Equatable { required this.originalFileName, this.archivedFileName, this.storagePath, + this.searchHit, }); factory DocumentModel.fromJson(Map json) => diff --git a/packages/paperless_api/lib/src/models/document_model.g.dart b/packages/paperless_api/lib/src/models/document_model.g.dart index 90c6fa8..83df5ed 100644 --- a/packages/paperless_api/lib/src/models/document_model.g.dart +++ b/packages/paperless_api/lib/src/models/document_model.g.dart @@ -25,21 +25,34 @@ DocumentModel _$DocumentModelFromJson(Map json) => originalFileName: json['original_file_name'] as String, archivedFileName: json['archived_file_name'] as String?, storagePath: json['storage_path'] as int?, + searchHit: json['__search_hit__'] == null + ? null + : SearchHit.fromJson(json['__search_hit__'] as Map), ); -Map _$DocumentModelToJson(DocumentModel instance) => - { - 'id': instance.id, - 'title': instance.title, - 'content': instance.content, - 'tags': instance.tags.toList(), - 'document_type': instance.documentType, - 'correspondent': instance.correspondent, - 'storage_path': instance.storagePath, - 'created': const LocalDateTimeJsonConverter().toJson(instance.created), - 'modified': const LocalDateTimeJsonConverter().toJson(instance.modified), - 'added': const LocalDateTimeJsonConverter().toJson(instance.added), - 'archive_serial_number': instance.archiveSerialNumber, - 'original_file_name': instance.originalFileName, - 'archived_file_name': instance.archivedFileName, - }; +Map _$DocumentModelToJson(DocumentModel instance) { + final val = { + 'id': instance.id, + 'title': instance.title, + 'content': instance.content, + 'tags': instance.tags.toList(), + 'document_type': instance.documentType, + 'correspondent': instance.correspondent, + 'storage_path': instance.storagePath, + 'created': const LocalDateTimeJsonConverter().toJson(instance.created), + 'modified': const LocalDateTimeJsonConverter().toJson(instance.modified), + 'added': const LocalDateTimeJsonConverter().toJson(instance.added), + 'archive_serial_number': instance.archiveSerialNumber, + 'original_file_name': instance.originalFileName, + 'archived_file_name': instance.archivedFileName, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('__search_hit__', instance.searchHit); + return val; +} diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index 3d43bd0..7ba46a8 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -21,7 +21,6 @@ export 'paperless_server_exception.dart'; export 'paperless_server_information_model.dart'; export 'paperless_server_statistics_model.dart'; export 'saved_view_model.dart'; -export 'similar_document_model.dart'; export 'task/task.dart'; export 'task/task_status.dart'; export 'field_suggestions.dart'; diff --git a/packages/paperless_api/lib/src/models/similar_document_model.dart b/packages/paperless_api/lib/src/models/similar_document_model.dart deleted file mode 100644 index 173b270..0000000 --- a/packages/paperless_api/lib/src/models/similar_document_model.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; -import 'package:paperless_api/src/models/document_model.dart'; -import 'package:paperless_api/src/models/search_hit.dart'; - -part 'similar_document_model.g.dart'; - -@LocalDateTimeJsonConverter() -@JsonSerializable() -class SimilarDocumentModel extends DocumentModel { - @JsonKey(name: '__search_hit__') - final SearchHit searchHit; - - const SimilarDocumentModel({ - required super.id, - required super.title, - required super.documentType, - required super.correspondent, - required super.created, - required super.modified, - required super.added, - required super.originalFileName, - required this.searchHit, - super.archiveSerialNumber, - super.archivedFileName, - super.content, - super.storagePath, - super.tags, - }); - - factory SimilarDocumentModel.fromJson(Map json) => - _$SimilarDocumentModelFromJson(json); - - @override - Map toJson() => _$SimilarDocumentModelToJson(this); -} diff --git a/packages/paperless_api/lib/src/models/similar_document_model.g.dart b/packages/paperless_api/lib/src/models/similar_document_model.g.dart deleted file mode 100644 index d2f996d..0000000 --- a/packages/paperless_api/lib/src/models/similar_document_model.g.dart +++ /dev/null @@ -1,50 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'similar_document_model.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SimilarDocumentModel _$SimilarDocumentModelFromJson( - Map json) => - SimilarDocumentModel( - id: json['id'] as int, - title: json['title'] as String, - documentType: json['documentType'] as int?, - correspondent: json['correspondent'] as int?, - created: const LocalDateTimeJsonConverter() - .fromJson(json['created'] as String), - modified: const LocalDateTimeJsonConverter() - .fromJson(json['modified'] as String), - added: - const LocalDateTimeJsonConverter().fromJson(json['added'] as String), - originalFileName: json['originalFileName'] as String, - searchHit: - SearchHit.fromJson(json['__search_hit__'] as Map), - archiveSerialNumber: json['archiveSerialNumber'] as int?, - archivedFileName: json['archivedFileName'] as String?, - content: json['content'] as String?, - storagePath: json['storagePath'] as int?, - tags: (json['tags'] as List?)?.map((e) => e as int) ?? - const [], - ); - -Map _$SimilarDocumentModelToJson( - SimilarDocumentModel instance) => - { - 'id': instance.id, - 'title': instance.title, - 'content': instance.content, - 'tags': instance.tags.toList(), - 'documentType': instance.documentType, - 'correspondent': instance.correspondent, - 'storagePath': instance.storagePath, - 'created': const LocalDateTimeJsonConverter().toJson(instance.created), - 'modified': const LocalDateTimeJsonConverter().toJson(instance.modified), - 'added': const LocalDateTimeJsonConverter().toJson(instance.added), - 'archiveSerialNumber': instance.archiveSerialNumber, - 'originalFileName': instance.originalFileName, - 'archivedFileName': instance.archivedFileName, - '__search_hit__': instance.searchHit, - }; 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 340469b..aabd746 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 @@ -18,7 +18,6 @@ abstract class PaperlessDocumentsApi { Future findNextAsn(); Future> findAll(DocumentFilter filter); Future find(int id); - Future> findSimilar(int docId); Future delete(DocumentModel doc); Future getMetaData(DocumentModel document); Future> bulkAction(BulkAction action); 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 9b99bd7..0536ddb 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 @@ -241,27 +241,6 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { } } - @override - Future> findSimilar(int docId) async { - try { - final response = - await client.get("/api/documents/?more_like=$docId&pageSize=10"); - if (response.statusCode == 200) { - return (await compute( - PagedSearchResult.fromJsonSingleParam, - PagedSearchResultJsonSerializer( - response.data, - SimilarDocumentModelJsonConverter(), - ), - )) - .results; - } - throw const PaperlessServerException(ErrorCode.similarQueryError); - } on DioError catch (err) { - throw err.error; - } - } - @override Future findSuggestions(DocumentModel document) async { try { From 9bfb6aa6618e0c8ceaadaa9ce7cc14e278de5063 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sun, 22 Jan 2023 14:34:58 +0100 Subject: [PATCH 06/25] Removed unused files, code cleanup --- .../paperless_server_information_cubit.dart | 1 - .../model/paperless_statistics_state.dart | 11 -- lib/core/security/security_context_utils.dart | 0 lib/extensions/date_time_extensions.dart | 5 - .../hydrated_storage_extension.dart | 1 - .../security_context_extension.dart | 22 --- lib/extensions/string_extensions.dart | 7 - .../widgets/welcome_intro_slide.dart | 27 --- .../view/pages/document_details_page.dart | 2 +- .../view/pages/similar_documents_view.dart | 1 + .../widgets/document_download_button.dart | 1 + .../document_upload_preparation_page.dart | 1 + .../view/pages/document_edit_page.dart | 1 + .../documents/view/pages/documents_page.dart | 1 + .../view/widgets/order_by_dropdown.dart | 21 -- .../selection/documents_page_app_bar.dart | 156 --------------- .../edit_label/view/edit_label_page.dart | 1 + lib/features/edit_label/view/label_form.dart | 1 + lib/features/home/view/home_page.dart | 7 +- lib/features/home/view/widget/app_drawer.dart | 1 + lib/features/inbox/view/pages/inbox_page.dart | 1 + .../form_builder_tag_selection_field.dart | 1 - .../labels/view/widgets/label_item.dart | 14 +- .../bloc/linked_documents_cubit.dart | 2 +- .../bloc/state/linked_documents_state.dart | 0 .../view/pages/linked_documents_page.dart | 4 +- lib/features/login/view/login_page.dart | 1 + .../view/saved_view_selection_widget.dart | 1 + lib/features/scan/view/scanner_page.dart | 9 +- ...em_widget.dart => scanned_image_item.dart} | 8 +- lib/features/scan/view/widgets/scanner.dart | 42 ---- .../scan/view/widgets/upload_dialog.dart | 65 ------- lib/features/tasks/cubit/tasks_cubit.dart | 8 - lib/features/tasks/cubit/tasks_state.dart | 10 - lib/helpers/file_helpers.dart | 3 + lib/helpers/format_helpers.dart | 6 + lib/helpers/image_helpers.dart | 38 ++++ lib/helpers/message_helpers.dart | 111 +++++++++++ lib/helpers/permission_helpers.dart | 14 ++ lib/util.dart | 182 ------------------ pubspec.lock | 48 ++++- pubspec.yaml | 1 + 42 files changed, 248 insertions(+), 589 deletions(-) delete mode 100644 lib/core/model/paperless_statistics_state.dart delete mode 100644 lib/core/security/security_context_utils.dart delete mode 100644 lib/extensions/date_time_extensions.dart delete mode 100644 lib/extensions/security_context_extension.dart delete mode 100644 lib/extensions/string_extensions.dart delete mode 100644 lib/features/app_intro/widgets/welcome_intro_slide.dart delete mode 100644 lib/features/documents/view/widgets/order_by_dropdown.dart delete mode 100644 lib/features/documents/view/widgets/selection/documents_page_app_bar.dart delete mode 100644 lib/features/labels/tags/view/widgets/form_builder_tag_selection_field.dart rename lib/features/{linked_documents_preview => linked_documents}/bloc/linked_documents_cubit.dart (84%) rename lib/features/{linked_documents_preview => linked_documents}/bloc/state/linked_documents_state.dart (100%) rename lib/features/{linked_documents_preview => linked_documents}/view/pages/linked_documents_page.dart (93%) rename lib/features/scan/view/widgets/{grid_image_item_widget.dart => scanned_image_item.dart} (94%) delete mode 100644 lib/features/scan/view/widgets/scanner.dart delete mode 100644 lib/features/scan/view/widgets/upload_dialog.dart delete mode 100644 lib/features/tasks/cubit/tasks_cubit.dart delete mode 100644 lib/features/tasks/cubit/tasks_state.dart create mode 100644 lib/helpers/file_helpers.dart create mode 100644 lib/helpers/format_helpers.dart create mode 100644 lib/helpers/image_helpers.dart create mode 100644 lib/helpers/message_helpers.dart create mode 100644 lib/helpers/permission_helpers.dart diff --git a/lib/core/bloc/paperless_server_information_cubit.dart b/lib/core/bloc/paperless_server_information_cubit.dart index 3c48eca..3067d8f 100644 --- a/lib/core/bloc/paperless_server_information_cubit.dart +++ b/lib/core/bloc/paperless_server_information_cubit.dart @@ -1,7 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; -import 'package:paperless_mobile/core/security/session_manager.dart'; class PaperlessServerInformationCubit extends Cubit { diff --git a/lib/core/model/paperless_statistics_state.dart b/lib/core/model/paperless_statistics_state.dart deleted file mode 100644 index 12fede2..0000000 --- a/lib/core/model/paperless_statistics_state.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:paperless_api/paperless_api.dart'; - -class PaperlessStatisticsState { - final bool isLoaded; - final PaperlessServerStatisticsModel? statistics; - - PaperlessStatisticsState({ - required this.isLoaded, - this.statistics, - }); -} diff --git a/lib/core/security/security_context_utils.dart b/lib/core/security/security_context_utils.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/extensions/date_time_extensions.dart b/lib/extensions/date_time_extensions.dart deleted file mode 100644 index 657c071..0000000 --- a/lib/extensions/date_time_extensions.dart +++ /dev/null @@ -1,5 +0,0 @@ -extension DateComparisons on DateTime { - bool isEqualToIgnoringDate(DateTime other) { - return day == other.day && month == other.month && year == other.year; - } -} diff --git a/lib/extensions/hydrated_storage_extension.dart b/lib/extensions/hydrated_storage_extension.dart index ef241e1..3646bea 100644 --- a/lib/extensions/hydrated_storage_extension.dart +++ b/lib/extensions/hydrated_storage_extension.dart @@ -1,6 +1,5 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_state.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; extension AddressableHydratedStorage on Storage { diff --git a/lib/extensions/security_context_extension.dart b/lib/extensions/security_context_extension.dart deleted file mode 100644 index 26e7789..0000000 --- a/lib/extensions/security_context_extension.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:io'; - -import 'package:paperless_mobile/features/login/model/client_certificate.dart'; - -extension ClientCertificateHandlingSecurityContext on SecurityContext { - SecurityContext withClientCertificate(ClientCertificate? clientCertificate) { - if (clientCertificate == null) return this; - return this - ..usePrivateKeyBytes( - clientCertificate.bytes, - password: clientCertificate.passphrase, - ) - ..useCertificateChainBytes( - clientCertificate.bytes, - password: clientCertificate.passphrase, - ) - ..setTrustedCertificatesBytes( - clientCertificate.bytes, - password: clientCertificate.passphrase, - ); - } -} diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart deleted file mode 100644 index 9e5a9b6..0000000 --- a/lib/extensions/string_extensions.dart +++ /dev/null @@ -1,7 +0,0 @@ -extension SizeLimitedString on String { - String withLengthLimitedTo(int length, [String overflow = "..."]) { - return this.length > length - ? '${substring(0, length - overflow.length)}$overflow' - : this; - } -} diff --git a/lib/features/app_intro/widgets/welcome_intro_slide.dart b/lib/features/app_intro/widgets/welcome_intro_slide.dart deleted file mode 100644 index a1fc625..0000000 --- a/lib/features/app_intro/widgets/welcome_intro_slide.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class WelcomeIntroSlide extends StatelessWidget { - const WelcomeIntroSlide({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Text( - "Welcome to Paperless Mobile!", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - Padding( - padding: const EdgeInsets.all(16), - child: Text( - "Manage, share and create documents on the go without any compromises!", - textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).hintColor), - ), - ), - Align(child: Image.asset("assets/logos/paperless_logo_green.png")), - ], - ); - } -} 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 d8d7535..5fa8ae6 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -26,7 +26,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:badges/badges.dart' as b; diff --git a/lib/features/document_details/view/pages/similar_documents_view.dart b/lib/features/document_details/view/pages/similar_documents_view.dart index 70e7560..b426422 100644 --- a/lib/features/document_details/view/pages/similar_documents_view.dart +++ b/lib/features/document_details/view/pages/similar_documents_view.dart @@ -6,6 +6,7 @@ import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; class SimilarDocumentsView extends StatefulWidget { diff --git a/lib/features/document_details/view/widgets/document_download_button.dart b/lib/features/document_details/view/widgets/document_download_button.dart index 8e91de1..b719eda 100644 --- a/lib/features/document_details/view/widgets/document_download_button.dart +++ b/lib/features/document_details/view/widgets/document_download_button.dart @@ -5,6 +5,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; import 'package:provider/provider.dart'; 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 34974ac..de7b66e 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -19,6 +19,7 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; class DocumentUploadPreparationPage extends StatefulWidget { diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 9c63df4..44fa050 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -20,6 +20,7 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; class DocumentEditPage extends StatefulWidget { diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 6306c0e..d615a2b 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -26,6 +26,7 @@ import 'package:paperless_mobile/features/settings/model/application_settings_st import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; class DocumentFilterIntent { diff --git a/lib/features/documents/view/widgets/order_by_dropdown.dart b/lib/features/documents/view/widgets/order_by_dropdown.dart deleted file mode 100644 index 2432e6e..0000000 --- a/lib/features/documents/view/widgets/order_by_dropdown.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_api/paperless_api.dart'; - -class OrderByDropdown extends StatefulWidget { - static const fkOrderBy = "orderBy"; - const OrderByDropdown({super.key}); - - @override - State createState() => _OrderByDropdownState(); -} - -class _OrderByDropdownState extends State { - @override - Widget build(BuildContext context) { - return FormBuilderDropdown( - name: OrderByDropdown.fkOrderBy, - items: const [], - ); - } -} diff --git a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart deleted file mode 100644 index 4303a73..0000000 --- a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/widgets/offline_banner.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; -import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; - -class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget { - final List actions; - final bool isOffline; - - const DocumentsPageAppBar({ - super.key, - required this.isOffline, - this.actions = const [], - }); - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); - @override - State createState() => _DocumentsPageAppBarState(); -} - -class _DocumentsPageAppBarState extends State { - @override - Widget build(BuildContext context) { - const savedViewWidgetHeight = 48.0; - final flexibleAreaHeight = kToolbarHeight - - 16 + - savedViewWidgetHeight + - (widget.isOffline ? 24 : 0); - return BlocBuilder( - builder: (context, documentsState) { - final hasSelection = documentsState.selection.isNotEmpty; - // final PreferredSize? loadingWidget = documentsState.isLoading - // ? const PreferredSize( - // child: LinearProgressIndicator(), - // preferredSize: Size.fromHeight(4.0), - // ) - // : null; - if (hasSelection) { - return SliverAppBar( - // bottom: loadingWidget, - expandedHeight: kToolbarHeight + flexibleAreaHeight, - snap: true, - floating: true, - pinned: true, - flexibleSpace: _buildFlexibleArea( - false, - documentsState.filter, - savedViewWidgetHeight, - ), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => context.read().resetSelection(), - ), - title: Text( - '${documentsState.selection.length} ${S.of(context).documentsSelectedText}'), - actions: [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _onDelete(context, documentsState), - ), - ], - ); - } else { - return SliverAppBar( - // bottom: loadingWidget, - expandedHeight: kToolbarHeight + flexibleAreaHeight, - snap: true, - floating: true, - pinned: true, - flexibleSpace: _buildFlexibleArea( - true, - documentsState.filter, - savedViewWidgetHeight, - ), - title: Text( - '${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})', - ), - actions: [ - ...widget.actions, - ], - ); - } - }, - ); - } - - Widget _buildFlexibleArea( - bool enabled, - DocumentFilter filter, - double savedViewHeight, - ) { - return FlexibleSpaceBar( - background: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (widget.isOffline) const OfflineBanner(), - SavedViewSelectionWidget( - height: savedViewHeight, - enabled: enabled, - currentFilter: filter, - ).paddedSymmetrically(horizontal: 8.0), - ], - ), - ); - } - - void _onDelete(BuildContext context, DocumentsState documentsState) async { - final shouldDelete = await showDialog( - context: context, - builder: (context) => - BulkDeleteConfirmationDialog(state: documentsState)) ?? - false; - if (shouldDelete) { - try { - await context - .read() - .bulkRemove(documentsState.selection); - showSnackBar( - context, - S.of(context).documentsPageBulkDeleteSuccessfulText, - ); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - } - - String _formatDocumentCount(int count) { - return count > 99 ? "99+" : count.toString(); - } -} - -class ScrollListener extends ChangeNotifier { - double top = 0; - double _last = 0; - - ScrollListener.initialise(ScrollController controller, [double height = 56]) { - controller.addListener(() { - final current = controller.offset; - top += _last - current; - if (top <= -height) top = -height; - if (top >= 0) top = 0; - _last = current; - if (top <= 0 && top >= -height) notifyListeners(); - }); - } -} diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index d5b27d7..1eb72eb 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -9,6 +9,7 @@ import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; class EditLabelPage extends StatelessWidget { diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index b284b51..2ddc1b5 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -6,6 +6,7 @@ import 'package:paperless_mobile/core/translation/matching_algorithm_localizatio import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; class SubmitButtonConfig { diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index ac8ce5a..b069f10 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -29,7 +29,8 @@ import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/helpers/file_helpers.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:path/path.dart' as p; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:responsive_builder/responsive_builder.dart'; @@ -136,7 +137,7 @@ class _HomePageState extends State { toastLength: Toast.LENGTH_LONG, ); } - } catch (e, stackTrace) { + } catch (e) { Fluttertoast.showToast( msg: S.of(context).receiveSharedFilePermissionDeniedMessage, toastLength: Toast.LENGTH_LONG, @@ -236,7 +237,6 @@ class _HomePageState extends State { builder: (context, sizingInformation) { if (!sizingInformation.isMobile) { return Scaffold( - key: rootScaffoldKey, drawer: const AppDrawer(), body: Row( children: [ @@ -257,7 +257,6 @@ class _HomePageState extends State { ); } return Scaffold( - key: rootScaffoldKey, bottomNavigationBar: NavigationBar( elevation: 4.0, selectedIndex: _currentIndex, diff --git a/lib/features/home/view/widget/app_drawer.dart b/lib/features/home/view/widget/app_drawer.dart index 4b87940..7113df3 100644 --- a/lib/features/home/view/widget/app_drawer.dart +++ b/lib/features/home/view/widget/app_drawer.dart @@ -20,6 +20,7 @@ import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; import 'package:url_launcher/link.dart'; import 'package:url_launcher/url_launcher_string.dart'; diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index a41cdc1..60077e3 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -13,6 +13,7 @@ import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; class InboxPage extends StatefulWidget { diff --git a/lib/features/labels/tags/view/widgets/form_builder_tag_selection_field.dart b/lib/features/labels/tags/view/widgets/form_builder_tag_selection_field.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/features/labels/tags/view/widgets/form_builder_tag_selection_field.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index 537aa7f..4fc6cb0 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; -import 'package:paperless_mobile/features/linked_documents_preview/view/pages/linked_documents_page.dart'; +import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart'; +import 'package:paperless_mobile/features/linked_documents/view/pages/linked_documents_page.dart'; +import 'package:paperless_mobile/helpers/format_helpers.dart'; class LabelItem extends StatelessWidget { final T label; @@ -37,7 +38,7 @@ class LabelItem extends StatelessWidget { Widget _buildReferencedDocumentsWidget(BuildContext context) { return TextButton.icon( label: const Icon(Icons.link), - icon: Text(_formatDocumentCount(label.documentCount)), + icon: Text(formatMaxCount(label.documentCount)), onPressed: (label.documentCount ?? 0) == 0 ? null : () { @@ -57,11 +58,4 @@ class LabelItem extends StatelessWidget { }, ); } - - String _formatDocumentCount(int? count) { - if ((count ?? 0) > 99) { - return "99+"; - } - return (count ?? 0).toString().padLeft(3); - } } diff --git a/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart b/lib/features/linked_documents/bloc/linked_documents_cubit.dart similarity index 84% rename from lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart rename to lib/features/linked_documents/bloc/linked_documents_cubit.dart index cc1fd3b..cf77aa6 100644 --- a/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart +++ b/lib/features/linked_documents/bloc/linked_documents_cubit.dart @@ -1,6 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; +import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; class LinkedDocumentsCubit extends Cubit { final PaperlessDocumentsApi _api; diff --git a/lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart b/lib/features/linked_documents/bloc/state/linked_documents_state.dart similarity index 100% rename from lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart rename to lib/features/linked_documents/bloc/state/linked_documents_state.dart diff --git a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart b/lib/features/linked_documents/view/pages/linked_documents_page.dart similarity index 93% rename from lib/features/linked_documents_preview/view/pages/linked_documents_page.dart rename to lib/features/linked_documents/view/pages/linked_documents_page.dart index fa39d38..5cce90f 100644 --- a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents/view/pages/linked_documents_page.dart @@ -5,8 +5,8 @@ import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; -import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; -import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; +import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart'; +import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class LinkedDocumentsPage extends StatefulWidget { diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index f49baee..1e3f343 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -9,6 +9,7 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_ import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; import 'widgets/never_scrollable_scroll_behavior.dart'; diff --git a/lib/features/saved_view/view/saved_view_selection_widget.dart b/lib/features/saved_view/view/saved_view_selection_widget.dart index 0d96af6..b25cfff 100644 --- a/lib/features/saved_view/view/saved_view_selection_widget.dart +++ b/lib/features/saved_view/view/saved_view_selection_widget.dart @@ -12,6 +12,7 @@ import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/util.dart'; import 'package:shimmer/shimmer.dart'; diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 7ae6bd8..9141da7 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -17,17 +17,18 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; -import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; -import 'package:paperless_mobile/features/scan/view/widgets/grid_image_item_widget.dart'; +import 'package:paperless_mobile/features/scan/view/widgets/scanned_image_item.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/helpers/file_helpers.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/helpers/permission_helpers.dart'; import 'package:path/path.dart' as p; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; @@ -218,7 +219,7 @@ class _ScannerPageState extends State mainAxisSpacing: 10, ), itemBuilder: (context, index) { - return GridImageItemWidget( + return ScannedImageItem( file: scans[index], onDelete: () async { try { diff --git a/lib/features/scan/view/widgets/grid_image_item_widget.dart b/lib/features/scan/view/widgets/scanned_image_item.dart similarity index 94% rename from lib/features/scan/view/widgets/grid_image_item_widget.dart rename to lib/features/scan/view/widgets/scanned_image_item.dart index 6997bbb..6f10a26 100644 --- a/lib/features/scan/view/widgets/grid_image_item_widget.dart +++ b/lib/features/scan/view/widgets/scanned_image_item.dart @@ -7,7 +7,7 @@ import 'package:photo_view/photo_view.dart'; typedef DeleteCallback = void Function(); typedef OnImageOperation = void Function(File); -class GridImageItemWidget extends StatefulWidget { +class ScannedImageItem extends StatefulWidget { final File file; final DeleteCallback onDelete; //final OnImageOperation onImageOperation; @@ -15,7 +15,7 @@ class GridImageItemWidget extends StatefulWidget { final int index; final int totalNumberOfFiles; - const GridImageItemWidget({ + const ScannedImageItem({ Key? key, required this.file, required this.onDelete, @@ -25,10 +25,10 @@ class GridImageItemWidget extends StatefulWidget { }) : super(key: key); @override - State createState() => _GridImageItemWidgetState(); + State createState() => _ScannedImageItemState(); } -class _GridImageItemWidgetState extends State { +class _ScannedImageItemState extends State { @override Widget build(BuildContext context) { return GestureDetector( diff --git a/lib/features/scan/view/widgets/scanner.dart b/lib/features/scan/view/widgets/scanner.dart deleted file mode 100644 index 6b6e76a..0000000 --- a/lib/features/scan/view/widgets/scanner.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:paperless_mobile/util.dart'; -import 'package:permission_handler/permission_handler.dart'; - -typedef OnImageScannedCallback = void Function(File); - -class ScannerWidget extends StatefulWidget { - final OnImageScannedCallback onImageScannedCallback; - const ScannerWidget({ - Key? key, - required this.onImageScannedCallback, - }) : super(key: key); - - @override - _ScannerWidgetState createState() => _ScannerWidgetState(); -} - -class _ScannerWidgetState extends State { - List documents = List.empty(growable: true); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("Scan document")), - body: FutureBuilder( - future: askForPermission(Permission.camera), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } - if (snapshot.data!) { - return Container(); - } - return const Center( - child: Text("No camera permissions, please enable in settings!"), - ); - }), - ); - } -} diff --git a/lib/features/scan/view/widgets/upload_dialog.dart b/lib/features/scan/view/widgets/upload_dialog.dart deleted file mode 100644 index 619b0b2..0000000 --- a/lib/features/scan/view/widgets/upload_dialog.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -class UploadDialog extends StatefulWidget { - const UploadDialog({ - Key? key, - }) : super(key: key); - - @override - State createState() => _UploadDialogState(); -} - -class _UploadDialogState extends State { - late TextEditingController _controller; - final _formKey = GlobalKey(); - - @override - void initState() { - final DateFormat format = DateFormat("yyyy_MM_dd_hh_mm_ss"); - final today = format.format(DateTime.now()); - _controller = TextEditingController.fromValue( - TextEditingValue(text: "Scan_$today.pdf")); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text("Upload to paperless-ng"), - content: Form( - key: _formKey, - child: TextFormField( - controller: _controller, - validator: (text) { - if (text == null || text.isEmpty) { - return "Filename must be specified!"; - } - return null; - }, - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Cancel"), - ), - TextButton( - onPressed: () { - if (!_formKey.currentState!.validate()) { - return; - } - var txt = _controller.text; - if (!txt.endsWith(".pdf")) { - txt += ".pdf"; - } - Navigator.of(context).pop(txt); - }, - child: const Text("Upload"), - ), - ], - ); - } -} diff --git a/lib/features/tasks/cubit/tasks_cubit.dart b/lib/features/tasks/cubit/tasks_cubit.dart deleted file mode 100644 index a5056f3..0000000 --- a/lib/features/tasks/cubit/tasks_cubit.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; - -part 'tasks_state.dart'; - -class TasksCubit extends Cubit { - TasksCubit() : super(TasksInitial()); -} diff --git a/lib/features/tasks/cubit/tasks_state.dart b/lib/features/tasks/cubit/tasks_state.dart deleted file mode 100644 index d4ae6d8..0000000 --- a/lib/features/tasks/cubit/tasks_state.dart +++ /dev/null @@ -1,10 +0,0 @@ -part of 'tasks_cubit.dart'; - -abstract class TasksState extends Equatable { - const TasksState(); - - @override - List get props => []; -} - -class TasksInitial extends TasksState {} diff --git a/lib/helpers/file_helpers.dart b/lib/helpers/file_helpers.dart new file mode 100644 index 0000000..6c4577f --- /dev/null +++ b/lib/helpers/file_helpers.dart @@ -0,0 +1,3 @@ +String extractFilenameFromPath(String path) { + return path.split(RegExp('[./]')).reversed.skip(1).first; +} diff --git a/lib/helpers/format_helpers.dart b/lib/helpers/format_helpers.dart new file mode 100644 index 0000000..a768ad0 --- /dev/null +++ b/lib/helpers/format_helpers.dart @@ -0,0 +1,6 @@ +String formatMaxCount(int? count, [int maxCount = 99]) { + if ((count ?? 0) > maxCount) { + return "$maxCount+"; + } + return (count ?? 0).toString().padLeft(maxCount.toString().length); +} diff --git a/lib/helpers/image_helpers.dart b/lib/helpers/image_helpers.dart new file mode 100644 index 0000000..05e8de7 --- /dev/null +++ b/lib/helpers/image_helpers.dart @@ -0,0 +1,38 @@ +// Taken from https://github.com/flutter/flutter/issues/26127#issuecomment-782083060 +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +Future loadImage(ImageProvider provider) { + final config = ImageConfiguration( + bundle: rootBundle, + devicePixelRatio: window.devicePixelRatio, + platform: defaultTargetPlatform, + ); + final Completer completer = Completer(); + final ImageStream stream = provider.resolve(config); + + late final ImageStreamListener listener; + + listener = ImageStreamListener((ImageInfo image, bool sync) { + debugPrint("Image ${image.debugLabel} finished loading"); + completer.complete(); + stream.removeListener(listener); + }, onError: (dynamic exception, StackTrace? stackTrace) { + completer.complete(); + stream.removeListener(listener); + FlutterError.reportError(FlutterErrorDetails( + context: ErrorDescription('image failed to load'), + library: 'image resource service', + exception: exception, + stack: stackTrace, + silent: true, + )); + }); + + stream.addListener(listener); + return completer.future; +} diff --git a/lib/helpers/message_helpers.dart b/lib/helpers/message_helpers.dart new file mode 100644 index 0000000..3e4c8bb --- /dev/null +++ b/lib/helpers/message_helpers.dart @@ -0,0 +1,111 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/service/github_issue_service.dart'; +import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class SnackBarActionConfig { + final String label; + final VoidCallback onPressed; + + SnackBarActionConfig({ + required this.label, + required this.onPressed, + }); +} + +void showSnackBar( + BuildContext context, + String message, { + String? details, + SnackBarActionConfig? action, + Duration duration = const Duration(seconds: 5), +}) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: (details != null) + ? RichText( + maxLines: 5, + text: TextSpan( + text: message, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onInverseSurface, + ), + children: [ + TextSpan( + text: "\n$details", + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: 10, + ), + ), + ], + ), + ) + : Text(message), + action: action != null + ? SnackBarAction( + label: action.label, + onPressed: action.onPressed, + textColor: Theme.of(context).colorScheme.onInverseSurface, + ) + : null, + duration: duration, + ), + ); +} + +void showGenericError( + BuildContext context, + dynamic error, [ + StackTrace? stackTrace, +]) { + showSnackBar( + context, + error.toString(), + action: SnackBarActionConfig( + label: S.of(context).errorReportLabel, + onPressed: () => GithubIssueService.createIssueFromError( + context, + stackTrace: stackTrace, + ), + ), + ); + log( + "An error has occurred.", + error: error, + stackTrace: stackTrace, + time: DateTime.now(), + ); +} + +void showLocalizedError( + BuildContext context, + String localizedMessage, [ + StackTrace? stackTrace, +]) { + showSnackBar(context, localizedMessage); + log(localizedMessage, stackTrace: stackTrace); +} + +void showErrorMessage( + BuildContext context, + PaperlessServerException error, [ + StackTrace? stackTrace, +]) { + showSnackBar( + context, + translateError(context, error.code), + details: error.details, + ); + log( + "An error has occurred.", + error: error, + stackTrace: stackTrace, + time: DateTime.now(), + ); +} diff --git a/lib/helpers/permission_helpers.dart b/lib/helpers/permission_helpers.dart new file mode 100644 index 0000000..a849994 --- /dev/null +++ b/lib/helpers/permission_helpers.dart @@ -0,0 +1,14 @@ +import 'dart:developer'; + +import 'package:permission_handler/permission_handler.dart'; + +Future askForPermission(Permission permission) async { + final status = await permission.request(); + log("Permission requested, new status is $status"); + // If user has permanently declined permission, open settings. + if (status == PermissionStatus.permanentlyDenied) { + await openAppSettings(); + } + + return status == PermissionStatus.granted; +} diff --git a/lib/util.dart b/lib/util.dart index 6870bf6..8b13789 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -1,183 +1 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; -import 'dart:ui'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:intl/intl.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; -import 'package:paperless_mobile/core/service/github_issue_service.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:permission_handler/permission_handler.dart'; - -final dateFormat = DateFormat("yyyy-MM-dd"); -final GlobalKey rootScaffoldKey = GlobalKey(); - -class SnackBarActionConfig { - final String label; - final VoidCallback onPressed; - - SnackBarActionConfig({ - required this.label, - required this.onPressed, - }); -} - -void showSnackBar( - BuildContext context, - String message, { - String? details, - SnackBarActionConfig? action, - Duration duration = const Duration(seconds: 5), -}) { - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: (details != null) - ? RichText( - maxLines: 5, - text: TextSpan( - text: message, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onInverseSurface, - ), - children: [ - TextSpan( - text: "\n$details", - style: const TextStyle( - fontStyle: FontStyle.italic, - fontSize: 10, - ), - ), - ], - ), - ) - : Text(message), - action: action != null - ? SnackBarAction( - label: action.label, - onPressed: action.onPressed, - textColor: Theme.of(context).colorScheme.onInverseSurface, - ) - : null, - duration: duration, - ), - ); -} - -void showGenericError( - BuildContext context, - dynamic error, [ - StackTrace? stackTrace, -]) { - showSnackBar( - context, - error.toString(), - action: SnackBarActionConfig( - label: S.of(context).errorReportLabel, - onPressed: () => GithubIssueService.createIssueFromError( - context, - stackTrace: stackTrace, - ), - ), - ); - log( - "An error has occurred.", - error: error, - stackTrace: stackTrace, - time: DateTime.now(), - ); -} - -void showLocalizedError( - BuildContext context, - String localizedMessage, [ - StackTrace? stackTrace, -]) { - showSnackBar(context, localizedMessage); - log(localizedMessage, stackTrace: stackTrace); -} - -void showErrorMessage( - BuildContext context, - PaperlessServerException error, [ - StackTrace? stackTrace, -]) { - showSnackBar( - context, - translateError(context, error.code), - details: error.details, - ); - log( - "An error has occurred.", - error: error, - stackTrace: stackTrace, - time: DateTime.now(), - ); -} - -bool isNotNull(dynamic value) { - return value != null; -} - -String formatDate(DateTime date) { - return dateFormat.format(date); -} - -String? formatDateNullable(DateTime? date) { - if (date == null) return null; - return dateFormat.format(date); -} - -String extractFilenameFromPath(String path) { - return path.split(RegExp('[./]')).reversed.skip(1).first; -} - -// Taken from https://github.com/flutter/flutter/issues/26127#issuecomment-782083060 -Future loadImage(ImageProvider provider) { - final config = ImageConfiguration( - bundle: rootBundle, - devicePixelRatio: window.devicePixelRatio, - platform: defaultTargetPlatform, - ); - final Completer completer = Completer(); - final ImageStream stream = provider.resolve(config); - - late final ImageStreamListener listener; - - listener = ImageStreamListener((ImageInfo image, bool sync) { - debugPrint("Image ${image.debugLabel} finished loading"); - completer.complete(); - stream.removeListener(listener); - }, onError: (dynamic exception, StackTrace? stackTrace) { - completer.complete(); - stream.removeListener(listener); - FlutterError.reportError(FlutterErrorDetails( - context: ErrorDescription('image failed to load'), - library: 'image resource service', - exception: exception, - stack: stackTrace, - silent: true, - )); - }); - - stream.addListener(listener); - return completer.future; -} - -Future askForPermission(Permission permission) async { - final status = await permission.request(); - log("Permission requested, new status is $status"); - // If user has permanently declined permission, open settings. - if (status == PermissionStatus.permanentlyDenied) { - await openAppSettings(); - } - - return status == PermissionStatus.granted; -} diff --git a/pubspec.lock b/pubspec.lock index 1ae465a..09ea6a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + sha256: "8c7478991c7bbde2c1e18034ac697723176a5d3e7e0ca06c7f9aed69b6f388d7" url: "https://pub.dev" source: hosted - version: "52.0.0" + version: "51.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + sha256: "120fe7ce25377ba616bb210e7584983b163861f45d6ec446744d507e3943881b" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.3.1" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + url: "https://pub.dev" + source: hosted + version: "2.0.1" archive: dependency: transitive description: @@ -305,6 +321,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.17.2" + dart_code_metrics: + dependency: "direct dev" + description: + name: dart_code_metrics + sha256: "95f22e95638c0dfb0cb4e3ba45e00bb06dd509c98f06d4c0fa45340b0a5392e0" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + dart_code_metrics_presets: + dependency: transitive + description: + name: dart_code_metrics_presets + sha256: "43dc1fdcb424fc3aa79964304d09eeda4f199351c52cdc854f8228a9d0296b60" + url: "https://pub.dev" + source: hosted + version: "1.1.0" dart_style: dependency: transitive description: @@ -1276,6 +1308,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + pub_updater: + dependency: transitive + description: + name: pub_updater + sha256: "00e42b515aa046b171d05bbe2dd566c0feaab7808c33c5bacb5beff93cf16561" + url: "https://pub.dev" + source: hosted + version: "0.2.3" pubspec_parse: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 899daae..0f94483 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,6 +100,7 @@ dev_dependencies: intl_utils: ^2.7.0 flutter_lints: ^1.0.0 json_serializable: ^6.5.4 + dart_code_metrics: ^5.4.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From f6ecbae6e8f0e1456361b53ff7b4829da2722ba4 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 23 Jan 2023 02:24:01 +0100 Subject: [PATCH 07/25] WIP - started implementing quick search --- lib/constants.dart | 7 + lib/core/service/status_service.dart | 2 +- lib/core/store/local_vault.dart | 77 ----- ...lor_scheme_option_localization_mapper.dart | 13 + .../hydrated_storage_extension.dart | 2 +- .../bloc/document_details_cubit.dart | 20 +- .../view/pages/document_details_page.dart | 10 +- .../view/pages/similar_documents_view.dart | 2 +- .../widgets/document_download_button.dart | 18 +- .../cubit/document_search_cubit.dart | 38 +++ .../cubit/document_search_state.dart | 75 +++++ .../document_search_delegate.dart | 106 +++++++ .../cubit/document_upload_cubit.dart | 2 - .../document_upload_preparation_page.dart | 2 +- .../view/pages/document_edit_page.dart | 2 +- .../documents/view/pages/documents_page.dart | 43 ++- .../edit_label/view/edit_label_page.dart | 2 +- lib/features/edit_label/view/label_form.dart | 2 +- lib/features/home/view/home_page.dart | 1 - lib/features/home/view/route_description.dart | 6 +- lib/features/home/view/widget/app_drawer.dart | 300 ++++++++---------- lib/features/inbox/view/pages/inbox_page.dart | 2 +- .../services/authentication_service.dart | 3 - lib/features/login/view/login_page.dart | 2 +- .../client_certificate_form_field.dart | 2 +- .../view/saved_view_selection_widget.dart | 2 +- lib/features/scan/view/scanner_page.dart | 3 - .../bloc/application_settings_cubit.dart | 15 +- .../application_settings_state.dart | 16 +- .../application_settings_state.g.dart | 18 +- .../settings/model/color_scheme_option.dart | 4 + .../view/pages/application_settings_page.dart | 6 + .../view/pages/storage_settings_page.dart | 4 +- .../biometric_authentication_setting.dart | 2 +- .../view/widgets/clear_storage_setting.dart | 21 -- .../view/widgets/clear_storage_settings.dart | 70 ++++ .../widgets/color_scheme_option_setting.dart | 86 +++++ .../widgets/language_selection_setting.dart | 11 +- .../view/widgets/radio_settings_dialog.dart | 18 +- .../view/widgets/theme_mode_setting.dart | 16 +- lib/helpers/format_helpers.dart | 9 + lib/l10n/intl_cs.arb | 7 +- lib/l10n/intl_de.arb | 7 +- lib/l10n/intl_en.arb | 7 +- lib/l10n/intl_tr.arb | 7 +- lib/main.dart | 108 +++---- lib/theme.dart | 47 +++ lib/util.dart | 1 - pubspec.lock | 8 + pubspec.yaml | 1 + 50 files changed, 824 insertions(+), 409 deletions(-) create mode 100644 lib/constants.dart delete mode 100644 lib/core/store/local_vault.dart create mode 100644 lib/core/translation/color_scheme_option_localization_mapper.dart create mode 100644 lib/features/document_search/cubit/document_search_cubit.dart create mode 100644 lib/features/document_search/cubit/document_search_state.dart create mode 100644 lib/features/document_search/document_search_delegate.dart rename lib/features/settings/{model => bloc}/application_settings_state.dart (75%) rename lib/features/settings/{model => bloc}/application_settings_state.g.dart (63%) create mode 100644 lib/features/settings/model/color_scheme_option.dart delete mode 100644 lib/features/settings/view/widgets/clear_storage_setting.dart create mode 100644 lib/features/settings/view/widgets/clear_storage_settings.dart create mode 100644 lib/features/settings/view/widgets/color_scheme_option_setting.dart create mode 100644 lib/theme.dart delete mode 100644 lib/util.dart diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..2a86892 --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,7 @@ +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +// Globally accessible variables which are definitely initialized after main(). +late final PackageInfo packageInfo; +late final AndroidDeviceInfo? androidInfo; +late final IosDeviceInfo? iosInfo; diff --git a/lib/core/service/status_service.dart b/lib/core/service/status_service.dart index 76b2428..dce014d 100644 --- a/lib/core/service/status_service.dart +++ b/lib/core/service/status_service.dart @@ -8,7 +8,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/document_status_cubit.dart'; import 'package:paperless_mobile/core/model/document_processing_status.dart'; import 'package:paperless_mobile/features/login/model/authentication_information.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:web_socket_channel/io.dart'; abstract class StatusService { diff --git a/lib/core/store/local_vault.dart b/lib/core/store/local_vault.dart deleted file mode 100644 index 9859e8f..0000000 --- a/lib/core/store/local_vault.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:convert'; - -import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; -import 'package:flutter/foundation.dart'; -import 'package:paperless_mobile/core/type/types.dart'; -import 'package:paperless_mobile/features/login/model/authentication_information.dart'; -import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; - -abstract class LocalVault { - Future storeAuthenticationInformation(AuthenticationInformation auth); - Future loadAuthenticationInformation(); - Future loadCertificate(); - Future storeApplicationSettings(ApplicationSettingsState settings); - Future loadApplicationSettings(); - Future clear(); -} - -class LocalVaultImpl implements LocalVault { - static const applicationSettingsKey = "applicationSettings"; - static const authenticationKey = "authentication"; - - final EncryptedSharedPreferences sharedPreferences; - - LocalVaultImpl(this.sharedPreferences); - - @override - Future storeAuthenticationInformation( - AuthenticationInformation auth, - ) async { - await sharedPreferences.setString( - authenticationKey, - jsonEncode(auth.toJson()), - ); - } - - @override - Future loadAuthenticationInformation() async { - if ((await sharedPreferences.getString(authenticationKey)).isEmpty) { - return null; - } - return AuthenticationInformation.fromJson( - jsonDecode(await sharedPreferences.getString(authenticationKey)), - ); - } - - @override - Future loadCertificate() async { - return loadAuthenticationInformation() - .then((value) => value?.clientCertificate); - } - - @override - Future storeApplicationSettings(ApplicationSettingsState settings) { - return sharedPreferences.setString( - applicationSettingsKey, - jsonEncode(settings.toJson()), - ); - } - - @override - Future loadApplicationSettings() async { - final settings = await sharedPreferences.getString(applicationSettingsKey); - if (settings.isEmpty) { - return null; - } - return compute( - ApplicationSettingsState.fromJson, - jsonDecode(settings) as JSON, - ); - } - - @override - Future clear() { - return sharedPreferences.clear(); - } -} diff --git a/lib/core/translation/color_scheme_option_localization_mapper.dart b/lib/core/translation/color_scheme_option_localization_mapper.dart new file mode 100644 index 0000000..c4b3f76 --- /dev/null +++ b/lib/core/translation/color_scheme_option_localization_mapper.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +String translateColorSchemeOption( + BuildContext context, ColorSchemeOption option) { + switch (option) { + case ColorSchemeOption.classic: + return S.of(context).colorSchemeOptionClassic; + case ColorSchemeOption.dynamic: + return S.of(context).colorSchemeOptionDynamic; + } +} diff --git a/lib/extensions/hydrated_storage_extension.dart b/lib/extensions/hydrated_storage_extension.dart index 3646bea..96ba2da 100644 --- a/lib/extensions/hydrated_storage_extension.dart +++ b/lib/extensions/hydrated_storage_extension.dart @@ -1,6 +1,6 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_state.dart'; -import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; extension AddressableHydratedStorage on Storage { ApplicationSettingsState get settings { diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart index 71ed062..0752697 100644 --- a/lib/features/document_details/bloc/document_details_cubit.dart +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -1,8 +1,10 @@ import 'dart:developer'; import 'dart:io'; +import 'dart:typed_data'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -49,14 +51,18 @@ class DocumentDetailsCubit extends Cubit { } Future openDocumentInSystemViewer() async { - final downloadDir = await FileService.temporaryDirectory; + final cacheDir = await FileService.temporaryDirectory; + final metaData = await _api.getMetaData(state.document); - final docBytes = await _api.download(state.document); - File f = File('${downloadDir.path}/${metaData.mediaFilename}'); - f.createSync(recursive: true); - f.writeAsBytesSync(docBytes); - return OpenFilex.open(f.path, type: "application/pdf") - .then((value) => value.type); + final bytes = await _api.download(state.document); + + final file = File('${cacheDir.path}/${metaData.mediaFilename}') + ..createSync(recursive: true) + ..writeAsBytesSync(bytes); + + return OpenFilex.open(file.path, type: "application/pdf").then( + (value) => value.type, + ); } void replaceDocument(DocumentModel document) { 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 5fa8ae6..3a03f58 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -26,6 +26,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -556,15 +557,6 @@ class _DocumentDetailsPageState extends State { ); } - static String formatBytes(int bytes, int decimals) { - if (bytes <= 0) return "0 B"; - const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - var i = (log(bytes) / log(1024)).floor(); - return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + - ' ' + - suffixes[i]; - } - Widget _buildSimilarDocumentsView() { return const SimilarDocumentsView(); } diff --git a/lib/features/document_details/view/pages/similar_documents_view.dart b/lib/features/document_details/view/pages/similar_documents_view.dart index b426422..2fb2b53 100644 --- a/lib/features/document_details/view/pages/similar_documents_view.dart +++ b/lib/features/document_details/view/pages/similar_documents_view.dart @@ -7,7 +7,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/documents_empty import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class SimilarDocumentsView extends StatefulWidget { const SimilarDocumentsView({super.key}); diff --git a/lib/features/document_details/view/widgets/document_download_button.dart b/lib/features/document_details/view/widgets/document_download_button.dart index b719eda..ccaf894 100644 --- a/lib/features/document_details/view/widgets/document_download_button.dart +++ b/lib/features/document_details/view/widgets/document_download_button.dart @@ -6,7 +6,7 @@ import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:provider/provider.dart'; class DocumentDownloadButton extends StatefulWidget { @@ -48,20 +48,24 @@ class _DocumentDownloadButtonState extends State { return; } setState(() => _isDownloadPending = true); + final service = context.read(); try { - final bytes = - await context.read().download(document); + final bytes = await service.download(document); + final meta = await service.getMetaData(document); final Directory dir = await FileService.downloadsDirectory; - String filePath = "${dir.path}/${document.originalFileName}"; - //TODO: Add replacement mechanism here (ask user if file should be replaced if exists) - await File(filePath).writeAsBytes(bytes); + String filePath = "${dir.path}/${meta.mediaFilename}"; + final createdFile = File(filePath); + createdFile.createSync(recursive: true); + createdFile.writeAsBytesSync(bytes); showSnackBar(context, S.of(context).documentDownloadSuccessMessage); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } catch (error) { showGenericError(context, error); } finally { - setState(() => _isDownloadPending = false); + if (mounted) { + setState(() => _isDownloadPending = false); + } } } } diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart new file mode 100644 index 0000000..bd81066 --- /dev/null +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -0,0 +1,38 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_api/src/modules/documents_api/paperless_documents_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; + +import 'document_search_state.dart'; + +class DocumentSearchCubit extends HydratedCubit + with DocumentsPagingMixin { + DocumentSearchCubit(this.api) : super(const DocumentSearchState()); + + @override + final PaperlessDocumentsApi api; + + Future updateResults(String query) async { + await updateFilter( + filter: state.filter.copyWith(query: TextQuery.titleAndContent(query)), + ); + emit(state.copyWith(searchHistory: [query, ...state.searchHistory])); + } + + Future updateSuggestions(String query) async { + final suggestions = await api.autocomplete(query); + emit(state.copyWith(suggestions: suggestions)); + } + + @override + DocumentSearchState? fromJson(Map json) { + return DocumentSearchState.fromJson(json); + } + + @override + Map? toJson(DocumentSearchState state) { + return state.toJson(); + } +} diff --git a/lib/features/document_search/cubit/document_search_state.dart b/lib/features/document_search/cubit/document_search_state.dart new file mode 100644 index 0000000..a6b0be7 --- /dev/null +++ b/lib/features/document_search/cubit/document_search_state.dart @@ -0,0 +1,75 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; + +part 'document_search_state.g.dart'; + + + +@JsonSerializable(ignoreUnannotated: true) +class DocumentSearchState extends DocumentsPagedState { + @JsonKey() + final List searchHistory; + + final List suggestions; + + const DocumentSearchState({ + this.searchHistory = const [], + this.suggestions = const [], + super.filter, + super.hasLoaded, + super.isLoading, + super.value, + }); + + @override + List get props => [ + hasLoaded, + isLoading, + filter, + value, + searchHistory, + suggestions, + ]; + + @override + DocumentSearchState copyWithPaged({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return copyWith( + hasLoaded: hasLoaded, + isLoading: isLoading, + filter: filter, + value: value, + ); + } + + DocumentSearchState copyWith({ + List? searchHistory, + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + List? suggestions, + }) { + return DocumentSearchState( + value: value ?? this.value, + filter: filter ?? this.filter, + hasLoaded: hasLoaded ?? this.hasLoaded, + isLoading: isLoading ?? this.isLoading, + searchHistory: searchHistory ?? this.searchHistory, + suggestions: suggestions ?? this.suggestions, + ); + } + + factory DocumentSearchState.fromJson(Map json) => + _$DocumentSearchStateFromJson(json); + + Map toJson() => _$DocumentSearchStateToJson(this); +} + +class diff --git a/lib/features/document_search/document_search_delegate.dart b/lib/features/document_search/document_search_delegate.dart new file mode 100644 index 0000000..d70af8b --- /dev/null +++ b/lib/features/document_search/document_search_delegate.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; +import 'package:provider/provider.dart'; + +class DocumentSearchDelegate extends SearchDelegate { + DocumentSearchDelegate({ + required String hintText, + required super.searchFieldStyle, + }) : super( + searchFieldLabel: hintText, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.search, + ); + + @override + Widget buildLeading(BuildContext context) => const BackButton(); + + @override + Widget buildSuggestions(BuildContext context) { + BlocBuilder( + builder: (context, state) { + if (!state.hasLoaded && state.isLoading) { + return const DocumentsListLoadingWidget(); + } + return ListView.builder(itemBuilder: (context, index) => ListTile( + title: Text(snapshot.data![index]), + onTap: () { + query = snapshot.data![index]; + super.showResults(context); + }, + ),); + }, + ) + return FutureBuilder( + future: context.read().autocomplete(query), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return ListView.builder( + itemCount: snapshot.data!.length, + itemBuilder: (context, index) => ListTile( + title: Text(snapshot.data![index]), + onTap: () { + query = snapshot.data![index]; + super.showResults(context); + }, + ), + ); + }, + ); + } + + @override + Widget buildResults(BuildContext context) { + return FutureBuilder( + future: context + .read() + .findAll(DocumentFilter(query: TextQuery.titleAndContent(query))), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + final documents = snapshot.data!.results; + return ListView.builder( + itemBuilder: (context, index) => DocumentListItem( + document: documents[index], + onTap: (document) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => DocumentDetailsCubit( + context.read(), + document, + ), + child: const LabelRepositoriesProvider( + child: DocumentDetailsPage( + isLabelClickable: false, + ), + ), + ), + ), + ); + }, + ), + ); + }, + ); + } + + @override + List buildActions(BuildContext context) => []; +} diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart index fcc159c..dec42e2 100644 --- a/lib/features/document_upload/cubit/document_upload_cubit.dart +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -8,7 +8,6 @@ import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; part 'document_upload_state.dart'; @@ -24,7 +23,6 @@ class DocumentUploadCubit extends Cubit { final List _subs = []; DocumentUploadCubit({ - required LocalVault localVault, required PaperlessDocumentsApi documentApi, required LabelRepository tagRepository, required LabelRepository 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 de7b66e..dc91d9e 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -20,7 +20,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class DocumentUploadPreparationPage extends StatefulWidget { final Uint8List fileBytes; diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 44fa050..b854897 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -21,7 +21,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class DocumentEditPage extends StatefulWidget { final FieldSuggestions suggestions; diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index d615a2b..75c8da3 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -8,6 +8,7 @@ import 'package:paperless_mobile/core/repository/provider/label_repositories_pro import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_search/document_search_delegate.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; @@ -22,12 +23,12 @@ import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.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/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class DocumentFilterIntent { final DocumentFilter? filter; @@ -148,9 +149,42 @@ class _DocumentsPageState extends State { builder: (context, state) { if (state.selection.isEmpty) { return AppBar( - title: Text( - "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})", + title: TextField( + onTap: () => showSearch( + context: context, + delegate: DocumentSearchDelegate( + searchFieldStyle: + Theme.of(context).textTheme.bodyLarge, + hintText: "Search your documents", + ), + ), + readOnly: true, + decoration: InputDecoration( + hintText: "Search your documents", + hintStyle: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), + filled: true, + fillColor: Theme.of(context).colorScheme.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(32), + borderSide: BorderSide.none, + ), + prefixIcon: IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ), + ), ), + // title: Text( + // "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})", + // ), actions: [ const SortDocumentsButton(), BlocBuilder { ? const LinearProgressIndicator() : const SizedBox(height: 4.0), ), + automaticallyImplyLeading: false, ); } else { return AppBar( diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index 1eb72eb..28d2273 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -10,7 +10,7 @@ import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class EditLabelPage extends StatelessWidget { final T label; diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index 2ddc1b5..8fa7327 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -7,7 +7,7 @@ import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class SubmitButtonConfig { final Widget icon; diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index b069f10..fafd6bd 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -109,7 +109,6 @@ class _HomePageState extends State { MaterialPageRoute( builder: (context) => BlocProvider.value( value: DocumentUploadCubit( - localVault: context.read(), documentApi: context.read(), tagRepository: context.read(), correspondentRepository: context.read(), diff --git a/lib/features/home/view/route_description.dart b/lib/features/home/view/route_description.dart index ee4c36a..6fc36a6 100644 --- a/lib/features/home/view/route_description.dart +++ b/lib/features/home/view/route_description.dart @@ -4,11 +4,13 @@ class RouteDescription { final String label; final Icon icon; final Icon selectedIcon; + final Widget Function(Widget icon)? badgeBuilder; RouteDescription({ required this.label, required this.icon, required this.selectedIcon, + this.badgeBuilder, }); NavigationDestination toNavigationDestination() { @@ -30,8 +32,8 @@ class RouteDescription { BottomNavigationBarItem toBottomNavigationBarItem() { return BottomNavigationBarItem( label: label, - icon: icon, - activeIcon: selectedIcon, + icon: badgeBuilder?.call(icon) ?? icon, + activeIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon, ); } } diff --git a/lib/features/home/view/widget/app_drawer.dart b/lib/features/home/view/widget/app_drawer.dart index 7113df3..f6b5c98 100644 --- a/lib/features/home/view/widget/app_drawer.dart +++ b/lib/features/home/view/widget/app_drawer.dart @@ -12,7 +12,6 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; @@ -21,7 +20,7 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:url_launcher/link.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -43,12 +42,9 @@ class AppDrawer extends StatefulWidget { // } class _AppDrawerState extends State { - late final Future _packageInfo; - @override void initState() { super.initState(); - _packageInfo = PackageInfo.fromPlatform(); } @override @@ -120,162 +116,150 @@ class _AppDrawerState extends State { bottomRight: Radius.circular(16.0), ), ), - child: Theme( - data: Theme.of(context).copyWith( - listTileTheme: ListTileThemeData( - tileColor: Colors.transparent, - ), - ), - child: ListView( - children: [ - DrawerHeader( - padding: const EdgeInsets.only( - top: 8, - left: 8, - bottom: 0, - right: 8, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Image.asset( - 'assets/logos/paperless_logo_white.png', - height: 32, - width: 32, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ).paddedOnly(right: 8.0), - Text( - S.of(context).appTitleText, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ], - ), - Align( - alignment: Alignment.bottomRight, - child: BlocBuilder( - builder: (context, state) { - if (!state.isLoaded) { - return Container(); - } - final info = state.information!; - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - dense: true, - title: Text( - S - .of(context) - .appDrawerHeaderLoggedInAsText + - (info.username ?? '?'), - style: - Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - state.information!.host ?? '', - style: Theme.of(context) - .textTheme - .bodyMedium, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - Text( - '${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})', - style: Theme.of(context) - .textTheme - .bodySmall, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - ], - ), - isThreeLine: true, - ), - ], - ); - }, + child: ListView( + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + padding: const EdgeInsets.only( + top: 8, + left: 8, + bottom: 0, + right: 8, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset( + 'assets/logos/paperless_logo_white.png', + height: 32, + width: 32, + color: + Theme.of(context).colorScheme.onPrimaryContainer, + ).paddedOnly(right: 8.0), + Text( + S.of(context).appTitleText, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), + ], + ), + Align( + alignment: Alignment.bottomRight, + child: BlocBuilder( + builder: (context, state) { + if (!state.isLoaded) { + return Container(); + } + final info = state.information!; + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + dense: true, + title: Text( + S.of(context).appDrawerHeaderLoggedInAsText + + (info.username ?? '?'), + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + maxLines: 1, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + state.information!.host ?? '', + style: Theme.of(context) + .textTheme + .bodyMedium, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + maxLines: 1, + ), + Text( + '${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})', + style: + Theme.of(context).textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + maxLines: 1, + ), + ], + ), + isThreeLine: true, + ), + ], + ); + }, ), - ], + ), + ], + ), + ), + ...[ + ListTile( + title: Text(S.of(context).bottomNavInboxPageLabel), + leading: const Icon(Icons.inbox), + onTap: () => _onOpenInbox(), + shape: listtTileShape, + ), + ListTile( + leading: const Icon(Icons.settings), + shape: listtTileShape, + title: Text( + S.of(context).appDrawerSettingsLabel, ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: context.read(), + child: const SettingsPage(), + ), + ), ), ), - ...[ - ListTile( - title: Text(S.of(context).bottomNavInboxPageLabel), - leading: const Icon(Icons.inbox), - onTap: () => _onOpenInbox(), - shape: listtTileShape, - ), - ListTile( - leading: const Icon(Icons.settings), - shape: listtTileShape, - title: Text( - S.of(context).appDrawerSettingsLabel, - ), - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: context.read(), - child: const SettingsPage(), - ), - ), - ), - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ListTile( - leading: const Icon(Icons.bug_report), - title: Text(S.of(context).appDrawerReportBugLabel), - onTap: () { - launchUrlString( - 'https://github.com/astubenbord/paperless-mobile/issues/new'); - }, - shape: listtTileShape, - ), - ListTile( - title: Text(S.of(context).appDrawerAboutLabel), - leading: Icon(Icons.info_outline_rounded), - onTap: _onShowAboutDialog, - shape: listtTileShape, - ), - ListTile( - leading: const Icon(Icons.logout), - title: Text(S.of(context).appDrawerLogoutLabel), - shape: listtTileShape, - onTap: () { - _onLogout(); - }, - ) - ], + const Divider( + indent: 16, + endIndent: 16, + ), + ListTile( + leading: const Icon(Icons.bug_report), + title: Text(S.of(context).appDrawerReportBugLabel), + onTap: () { + launchUrlString( + 'https://github.com/astubenbord/paperless-mobile/issues/new'); + }, + shape: listtTileShape, + ), + ListTile( + title: Text(S.of(context).appDrawerAboutLabel), + leading: Icon(Icons.info_outline_rounded), + onTap: _onShowAboutDialog, + shape: listtTileShape, + ), + ListTile( + leading: const Icon(Icons.logout), + title: Text(S.of(context).appDrawerLogoutLabel), + shape: listtTileShape, + onTap: () { + _onLogout(); + }, + ) ], - ), + ], ), ), ), @@ -285,7 +269,6 @@ class _AppDrawerState extends State { void _onLogout() async { try { await context.read().logout(); - await context.read().clear(); await context.read().clear(); await context.read>().clear(); await context @@ -354,15 +337,14 @@ class _AppDrawerState extends State { ); } - Future _onShowAboutDialog() async { - final snapshot = await _packageInfo; + void _onShowAboutDialog() { showAboutDialog( context: context, applicationIcon: const ImageIcon( AssetImage('assets/logos/paperless_logo_green.png'), ), applicationName: 'Paperless Mobile', - applicationVersion: snapshot.version + '+' + snapshot.buildNumber, + applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, children: [ Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), Link( diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 60077e3..c464e53 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -14,7 +14,7 @@ import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget. import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; class InboxPage extends StatefulWidget { const InboxPage({super.key}); diff --git a/lib/features/login/services/authentication_service.dart b/lib/features/login/services/authentication_service.dart index 84aa8c4..6600125 100644 --- a/lib/features/login/services/authentication_service.dart +++ b/lib/features/login/services/authentication_service.dart @@ -1,12 +1,9 @@ import 'package:local_auth/local_auth.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; class LocalAuthenticationService { - final LocalVault localStore; final LocalAuthentication localAuthentication; LocalAuthenticationService( - this.localStore, this.localAuthentication, ); diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index 1e3f343..30edca1 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -10,7 +10,7 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_cr import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'widgets/never_scrollable_scroll_behavior.dart'; import 'widgets/login_pages/server_login_page.dart'; diff --git a/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart index 408577b..a77605f 100644 --- a/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart @@ -6,7 +6,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:permission_handler/permission_handler.dart'; import 'obscured_input_text_form_field.dart'; diff --git a/lib/features/saved_view/view/saved_view_selection_widget.dart b/lib/features/saved_view/view/saved_view_selection_widget.dart index b25cfff..bbc21b8 100644 --- a/lib/features/saved_view/view/saved_view_selection_widget.dart +++ b/lib/features/saved_view/view/saved_view_selection_widget.dart @@ -13,7 +13,7 @@ import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/util.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:shimmer/shimmer.dart'; class SavedViewSelectionWidget extends StatelessWidget { diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 9141da7..37f2776 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -16,7 +16,6 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; @@ -148,7 +147,6 @@ class _ScannerPageState extends State builder: (_) => LabelRepositoriesProvider( child: BlocProvider( create: (context) => DocumentUploadCubit( - localVault: context.read(), documentApi: context.read(), correspondentRepository: context.read< LabelRepository builder: (_) => LabelRepositoriesProvider( child: BlocProvider( create: (context) => DocumentUploadCubit( - localVault: context.read(), documentApi: context.read(), correspondentRepository: context.read< LabelRepository { @@ -27,17 +28,23 @@ class ApplicationSettingsCubit extends HydratedCubit { } } - Future setThemeMode(ThemeMode? selectedMode) async { + void setThemeMode(ThemeMode? selectedMode) { final updatedSettings = state.copyWith(preferredThemeMode: selectedMode); _updateSettings(updatedSettings); } - Future setViewType(ViewType viewType) async { + void setViewType(ViewType viewType) { final updatedSettings = state.copyWith(preferredViewType: viewType); _updateSettings(updatedSettings); } - Future _updateSettings(ApplicationSettingsState settings) async { + void setColorSchemeOption(ColorSchemeOption schemeOption) { + final updatedSettings = + state.copyWith(preferredColorSchemeOption: schemeOption); + _updateSettings(updatedSettings); + } + + void _updateSettings(ApplicationSettingsState settings) async { emit(settings); } diff --git a/lib/features/settings/model/application_settings_state.dart b/lib/features/settings/bloc/application_settings_state.dart similarity index 75% rename from lib/features/settings/model/application_settings_state.dart rename to lib/features/settings/bloc/application_settings_state.dart index 05c284d..7dfc48f 100644 --- a/lib/features/settings/model/application_settings_state.dart +++ b/lib/features/settings/bloc/application_settings_state.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_mobile/core/type/types.dart'; +import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; part 'application_settings_state.g.dart'; @@ -13,22 +13,21 @@ part 'application_settings_state.g.dart'; @JsonSerializable() class ApplicationSettingsState { static final defaultSettings = ApplicationSettingsState( - isLocalAuthenticationEnabled: false, preferredLocaleSubtag: Platform.localeName.split('_').first, - preferredThemeMode: ThemeMode.system, - preferredViewType: ViewType.list, ); final bool isLocalAuthenticationEnabled; final String preferredLocaleSubtag; final ThemeMode preferredThemeMode; final ViewType preferredViewType; + final ColorSchemeOption preferredColorSchemeOption; ApplicationSettingsState({ required this.preferredLocaleSubtag, - required this.preferredThemeMode, - required this.isLocalAuthenticationEnabled, - required this.preferredViewType, + this.preferredThemeMode = ThemeMode.system, + this.isLocalAuthenticationEnabled = false, + this.preferredViewType = ViewType.list, + this.preferredColorSchemeOption = ColorSchemeOption.dynamic, }); Map toJson() => _$ApplicationSettingsStateToJson(this); @@ -40,6 +39,7 @@ class ApplicationSettingsState { String? preferredLocaleSubtag, ThemeMode? preferredThemeMode, ViewType? preferredViewType, + ColorSchemeOption? preferredColorSchemeOption, }) { return ApplicationSettingsState( isLocalAuthenticationEnabled: @@ -48,6 +48,8 @@ class ApplicationSettingsState { preferredLocaleSubtag ?? this.preferredLocaleSubtag, preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode, preferredViewType: preferredViewType ?? this.preferredViewType, + preferredColorSchemeOption: + preferredColorSchemeOption ?? this.preferredColorSchemeOption, ); } } diff --git a/lib/features/settings/model/application_settings_state.g.dart b/lib/features/settings/bloc/application_settings_state.g.dart similarity index 63% rename from lib/features/settings/model/application_settings_state.g.dart rename to lib/features/settings/bloc/application_settings_state.g.dart index 6166a38..a131830 100644 --- a/lib/features/settings/model/application_settings_state.g.dart +++ b/lib/features/settings/bloc/application_settings_state.g.dart @@ -11,11 +11,16 @@ ApplicationSettingsState _$ApplicationSettingsStateFromJson( ApplicationSettingsState( preferredLocaleSubtag: json['preferredLocaleSubtag'] as String, preferredThemeMode: - $enumDecode(_$ThemeModeEnumMap, json['preferredThemeMode']), + $enumDecodeNullable(_$ThemeModeEnumMap, json['preferredThemeMode']) ?? + ThemeMode.system, isLocalAuthenticationEnabled: - json['isLocalAuthenticationEnabled'] as bool, + json['isLocalAuthenticationEnabled'] as bool? ?? false, preferredViewType: - $enumDecode(_$ViewTypeEnumMap, json['preferredViewType']), + $enumDecodeNullable(_$ViewTypeEnumMap, json['preferredViewType']) ?? + ViewType.list, + preferredColorSchemeOption: $enumDecodeNullable( + _$ColorSchemeOptionEnumMap, json['preferredColorSchemeOption']) ?? + ColorSchemeOption.dynamic, ); Map _$ApplicationSettingsStateToJson( @@ -25,6 +30,8 @@ Map _$ApplicationSettingsStateToJson( 'preferredLocaleSubtag': instance.preferredLocaleSubtag, 'preferredThemeMode': _$ThemeModeEnumMap[instance.preferredThemeMode]!, 'preferredViewType': _$ViewTypeEnumMap[instance.preferredViewType]!, + 'preferredColorSchemeOption': + _$ColorSchemeOptionEnumMap[instance.preferredColorSchemeOption]!, }; const _$ThemeModeEnumMap = { @@ -37,3 +44,8 @@ const _$ViewTypeEnumMap = { ViewType.grid: 'grid', ViewType.list: 'list', }; + +const _$ColorSchemeOptionEnumMap = { + ColorSchemeOption.classic: 'classic', + ColorSchemeOption.dynamic: 'dynamic', +}; diff --git a/lib/features/settings/model/color_scheme_option.dart b/lib/features/settings/model/color_scheme_option.dart new file mode 100644 index 0000000..6bd92e3 --- /dev/null +++ b/lib/features/settings/model/color_scheme_option.dart @@ -0,0 +1,4 @@ +enum ColorSchemeOption { + classic, + dynamic; +} diff --git a/lib/features/settings/view/pages/application_settings_page.dart b/lib/features/settings/view/pages/application_settings_page.dart index b1b0720..6c8acb3 100644 --- a/lib/features/settings/view/pages/application_settings_page.dart +++ b/lib/features/settings/view/pages/application_settings_page.dart @@ -1,7 +1,12 @@ +import 'dart:developer'; +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/color_scheme_option_setting.dart'; import 'package:paperless_mobile/features/settings/view/widgets/language_selection_setting.dart'; import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setting.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/constants.dart'; class ApplicationSettingsPage extends StatelessWidget { const ApplicationSettingsPage({super.key}); @@ -16,6 +21,7 @@ class ApplicationSettingsPage extends StatelessWidget { children: const [ LanguageSelectionSetting(), ThemeModeSetting(), + ColorSchemeOptionSetting(), ], ), ); diff --git a/lib/features/settings/view/pages/storage_settings_page.dart b/lib/features/settings/view/pages/storage_settings_page.dart index 0aefad0..1dba0e8 100644 --- a/lib/features/settings/view/pages/storage_settings_page.dart +++ b/lib/features/settings/view/pages/storage_settings_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:paperless_mobile/features/settings/view/widgets/clear_storage_setting.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/clear_storage_settings.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class StorageSettingsPage extends StatelessWidget { @@ -13,7 +13,7 @@ class StorageSettingsPage extends StatelessWidget { ), body: ListView( children: const [ - ClearStorageSetting(), + ClearCacheSetting(), ], ), ); diff --git a/lib/features/settings/view/widgets/biometric_authentication_setting.dart b/lib/features/settings/view/widgets/biometric_authentication_setting.dart index 3a49d6e..b182f42 100644 --- a/lib/features/settings/view/widgets/biometric_authentication_setting.dart +++ b/lib/features/settings/view/widgets/biometric_authentication_setting.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.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/bloc/application_settings_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:provider/provider.dart'; diff --git a/lib/features/settings/view/widgets/clear_storage_setting.dart b/lib/features/settings/view/widgets/clear_storage_setting.dart deleted file mode 100644 index a956cfe..0000000 --- a/lib/features/settings/view/widgets/clear_storage_setting.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm; -import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:provider/provider.dart'; - -class ClearStorageSetting extends StatelessWidget { - const ClearStorageSetting({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text("Clear data"), - subtitle: - Text("Remove downloaded files, scans and clear the cache's content"), - onTap: () { - context.read().emptyCache(); - FileService.clearUserData(); - }, - ); - } -} diff --git a/lib/features/settings/view/widgets/clear_storage_settings.dart b/lib/features/settings/view/widgets/clear_storage_settings.dart new file mode 100644 index 0000000..78709bf --- /dev/null +++ b/lib/features/settings/view/widgets/clear_storage_settings.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm; +import 'package:paperless_mobile/core/service/file_service.dart'; +import 'package:paperless_mobile/helpers/format_helpers.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:provider/provider.dart'; + +class ClearCacheSetting extends StatelessWidget { + const ClearCacheSetting({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text("Clear downloaded files"), //TODO: INTL + subtitle: + Text("Deletes all files downloaded from this app."), //TODO: INTL + onTap: () async { + final dir = await FileService.downloadsDirectory; + final deletedSize = _dirSize(dir); + await dir.delete(recursive: true); + // await context.read().emptyCache(); + showSnackBar( + context, + "Downloads successfully cleared, removed $deletedSize.", + ); + }, + ); + } +} + +class ClearDownloadsSetting extends StatelessWidget { + const ClearDownloadsSetting({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text("Clear downloads"), //TODO: INTL + subtitle: Text( + "Remove downloaded files, scans and clear the cache's content"), //TODO: INTL + onTap: () { + FileService.documentsDirectory; + FileService.downloadsDirectory; + context.read().emptyCache(); + FileService.clearUserData(); + //TODO: Show notification about clearing (include size?) + }, + ); + } +} + +String _dirSize(Directory dir) { + int totalSize = 0; + try { + if (dir.existsSync()) { + dir + .listSync(recursive: true, followLinks: false) + .forEach((FileSystemEntity entity) { + if (entity is File) { + totalSize += entity.lengthSync(); + } + }); + } + } catch (e) { + print(e.toString()); + } + + return formatBytes(totalSize, 2); +} diff --git a/lib/features/settings/view/widgets/color_scheme_option_setting.dart b/lib/features/settings/view/widgets/color_scheme_option_setting.dart new file mode 100644 index 0000000..15a6566 --- /dev/null +++ b/lib/features/settings/view/widgets/color_scheme_option_setting.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart'; +import 'package:paperless_mobile/core/widgets/hint_card.dart'; +import 'package:paperless_mobile/features/login/services/authentication_service.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/constants.dart'; +import 'package:provider/provider.dart'; + +class ColorSchemeOptionSetting extends StatelessWidget { + const ColorSchemeOptionSetting({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, settings) { + return ListTile( + title: Text(S.of(context).settingsPageColorSchemeSettingLabel), + subtitle: Text( + translateColorSchemeOption( + context, + settings.preferredColorSchemeOption, + ), + ), + onTap: () => showDialog( + context: context, + builder: (_) => RadioSettingsDialog( + titleText: S.of(context).settingsPageColorSchemeSettingLabel, + descriptionText: + S.of(context).settingsPageColorSchemeSettingDialogDescription, + options: [ + RadioOption( + value: ColorSchemeOption.classic, + label: translateColorSchemeOption( + context, ColorSchemeOption.classic), + ), + RadioOption( + value: ColorSchemeOption.dynamic, + label: translateColorSchemeOption( + context, + ColorSchemeOption.dynamic, + ), + ), + ], + footer: _isBelowAndroid12() + ? HintCard( + hintText: S + .of(context) + .settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning, + hintIcon: Icons.warning_amber, + ) + : null, + initialValue: context + .read() + .state + .preferredColorSchemeOption, + ), + ).then( + (value) { + if (value != null) { + context + .read() + .setColorSchemeOption(value); + } + }, + ), + ); + }, + ); + } + + bool _isBelowAndroid12() { + if (Platform.isAndroid) { + final int version = + int.tryParse(androidInfo!.version.release ?? '0') ?? 0; + return version < 12; + } + return false; + } +} diff --git a/lib/features/settings/view/widgets/language_selection_setting.dart b/lib/features/settings/view/widgets/language_selection_setting.dart index b2b0a09..38470e4 100644 --- a/lib/features/settings/view/widgets/language_selection_setting.dart +++ b/lib/features/settings/view/widgets/language_selection_setting.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -30,7 +30,7 @@ class _LanguageSelectionSettingState extends State { onTap: () => showDialog( context: context, builder: (_) => RadioSettingsDialog( - title: Text(S.of(context).settingsPageLanguageSettingLabel), + titleText: S.of(context).settingsPageLanguageSettingLabel, options: [ RadioOption( value: 'en', @@ -54,8 +54,11 @@ class _LanguageSelectionSettingState extends State { .state .preferredLocaleSubtag, ), - ).then((value) => - context.read().setLocale(value)), + ).then((value) { + if (value != null) { + context.read().setLocale(value); + } + }), ); }, ); diff --git a/lib/features/settings/view/widgets/radio_settings_dialog.dart b/lib/features/settings/view/widgets/radio_settings_dialog.dart index 47c337c..c2d5d4c 100644 --- a/lib/features/settings/view/widgets/radio_settings_dialog.dart +++ b/lib/features/settings/view/widgets/radio_settings_dialog.dart @@ -4,7 +4,9 @@ import 'package:paperless_mobile/generated/l10n.dart'; class RadioSettingsDialog extends StatefulWidget { final List> options; final T initialValue; - final Widget? title; + final String? titleText; + final String? descriptionText; + final Widget? footer; final Widget? confirmButton; final Widget? cancelButton; @@ -12,9 +14,11 @@ class RadioSettingsDialog extends StatefulWidget { super.key, required this.options, required this.initialValue, - this.title, + this.titleText, this.confirmButton, this.cancelButton, + this.descriptionText, + this.footer, }); @override @@ -43,10 +47,16 @@ class _RadioSettingsDialogState extends State> { onPressed: () => Navigator.pop(context, _groupValue), child: Text(S.of(context).genericActionOkLabel)), ], - title: widget.title, + title: widget.titleText != null ? Text(widget.titleText!) : null, content: Column( mainAxisSize: MainAxisSize.min, - children: widget.options.map(_buildOptionListTile).toList(), + children: [ + if (widget.descriptionText != null) + Text(widget.descriptionText!, + style: Theme.of(context).textTheme.bodySmall), + ...widget.options.map(_buildOptionListTile), + if (widget.footer != null) widget.footer!, + ], ), ); } diff --git a/lib/features/settings/view/widgets/theme_mode_setting.dart b/lib/features/settings/view/widgets/theme_mode_setting.dart index 4430e2e..3c573ca 100644 --- a/lib/features/settings/view/widgets/theme_mode_setting.dart +++ b/lib/features/settings/view/widgets/theme_mode_setting.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -19,6 +19,11 @@ class ThemeModeSetting extends StatelessWidget { onTap: () => showDialog( context: context, builder: (_) => RadioSettingsDialog( + titleText: S.of(context).settingsPageAppearanceSettingTitle, + initialValue: context + .read() + .state + .preferredThemeMode, options: [ RadioOption( value: ThemeMode.system, @@ -38,14 +43,11 @@ class ThemeModeSetting extends StatelessWidget { S.of(context).settingsPageAppearanceSettingDarkThemeLabel, ) ], - initialValue: context - .read() - .state - .preferredThemeMode, - title: Text(S.of(context).settingsPageAppearanceSettingTitle), ), ).then((value) { - return context.read().setThemeMode(value); + if (value != null) { + context.read().setThemeMode(value); + } }), ); }, diff --git a/lib/helpers/format_helpers.dart b/lib/helpers/format_helpers.dart index a768ad0..5b863c4 100644 --- a/lib/helpers/format_helpers.dart +++ b/lib/helpers/format_helpers.dart @@ -1,6 +1,15 @@ +import 'dart:math'; + String formatMaxCount(int? count, [int maxCount = 99]) { if ((count ?? 0) > maxCount) { return "$maxCount+"; } return (count ?? 0).toString().padLeft(maxCount.toString().length); } + +String formatBytes(int bytes, int decimals) { + if (bytes <= 0) return "0 B"; + const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + var i = (log(bytes) / log(1024)).floor(); + return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i]; +} diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index b425bbc..98806d0 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -611,5 +611,10 @@ "verifyIdentityPageTitle": "Ověř svou identitu", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionDznamic": "Dynamic", + "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 02b9924..e32b4e3 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -611,5 +611,10 @@ "verifyIdentityPageTitle": "Verifiziere deine Identität", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionDznamic": "Dynamic", + "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 72a3ce5..ba3400d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -611,5 +611,10 @@ "verifyIdentityPageTitle": "Verify your identity", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionDynamic": "Dynamic", + "settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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." } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 61dd7c7..f6a1e0b 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -611,5 +611,10 @@ "verifyIdentityPageTitle": "Kimliğinizi doğrulayın", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionDznamic": "Dynamic", + "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 06e69ca..6cf6736 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'dart:developer'; +import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,6 +14,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl_standalone.dart'; import 'package:local_auth/local_auth.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; @@ -33,7 +36,6 @@ import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/dio_file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; @@ -43,29 +45,36 @@ import 'package:paperless_mobile/features/login/services/authentication_service. import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.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/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/theme.dart'; +import 'package:paperless_mobile/constants.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:dynamic_color/dynamic_color.dart'; void main() async { Bloc.observer = BlocChangesObserver(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); await findSystemLocale(); + packageInfo = await PackageInfo.fromPlatform(); + if (Platform.isAndroid) { + androidInfo = await DeviceInfoPlugin().androidInfo; + } + if (Platform.isIOS) { + iosInfo = await DeviceInfoPlugin().iosInfo; + } // Initialize External dependencies final connectivity = Connectivity(); - final encryptedSharedPreferences = EncryptedSharedPreferences(); final localAuthentication = LocalAuthentication(); // Initialize other utility classes final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity); - final localVault = LocalVaultImpl(encryptedSharedPreferences); - final localAuthService = - LocalAuthenticationService(localVault, localAuthentication); + final localAuthService = LocalAuthenticationService(localAuthentication); final hiveDir = await getApplicationDocumentsDirectory(); HydratedBloc.storage = await HydratedStorage.build( @@ -152,7 +161,6 @@ void main() async { ), ), ), - Provider.value(value: localVault), Provider.value( value: connectivityStatusService, ), @@ -207,22 +215,6 @@ class PaperlessMobileEntrypoint extends StatefulWidget { } class _PaperlessMobileEntrypointState extends State { - final _lightTheme = ThemeData.from( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.lightGreen, - brightness: Brightness.light, - ), - useMaterial3: true, - ); - - final _darkTheme = ThemeData.from( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.lightGreen, - brightness: Brightness.dark, - ), - useMaterial3: true, - ); - @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -235,52 +227,36 @@ class _PaperlessMobileEntrypointState extends State { ], child: BlocBuilder( builder: (context, settings) { - return MaterialApp( - debugShowCheckedModeBanner: true, - title: "Paperless Mobile", - theme: _lightTheme.copyWith( - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + return MaterialApp( + debugShowCheckedModeBanner: true, + title: "Paperless Mobile", + theme: buildTheme( + brightness: Brightness.light, + dynamicScheme: lightDynamic, + preferredColorScheme: settings.preferredColorSchemeOption, ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 16.0, + darkTheme: buildTheme( + brightness: Brightness.dark, + dynamicScheme: darkDynamic, + preferredColorScheme: settings.preferredColorSchemeOption, ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - listTileTheme: const ListTileThemeData( - tileColor: Colors.transparent, - ), - ), - darkTheme: _darkTheme.copyWith( - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), + themeMode: settings.preferredThemeMode, + supportedLocales: S.delegate.supportedLocales, + locale: Locale.fromSubtags( + languageCode: settings.preferredLocaleSubtag, ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 16.0, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - listTileTheme: const ListTileThemeData( - tileColor: Colors.transparent, - ), - ), - themeMode: settings.preferredThemeMode, - supportedLocales: S.delegate.supportedLocales, - locale: Locale.fromSubtags( - languageCode: settings.preferredLocaleSubtag, - ), - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - ], - home: const AuthenticationWrapper(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + ], + home: const AuthenticationWrapper(), + ); + }, ); }, ), diff --git a/lib/theme.dart b/lib/theme.dart new file mode 100644 index 0000000..172978b --- /dev/null +++ b/lib/theme.dart @@ -0,0 +1,47 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; + +const _classicThemeColorSeed = Colors.lightGreen; + +const _defaultListTileTheme = ListTileThemeData( + tileColor: Colors.transparent, +); + +final _defaultInputDecorationTheme = InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16.0, + ), +); + +ThemeData buildTheme({ + required Brightness brightness, + required ColorSchemeOption preferredColorScheme, + ColorScheme? dynamicScheme, +}) { + final classicScheme = ColorScheme.fromSeed( + seedColor: _classicThemeColorSeed, + brightness: brightness, + ); + late ColorScheme colorScheme; + switch (preferredColorScheme) { + case ColorSchemeOption.classic: + colorScheme = classicScheme; + break; + case ColorSchemeOption.dynamic: + colorScheme = dynamicScheme ?? classicScheme; + break; + } + return ThemeData.from( + colorScheme: colorScheme.harmonized(), + useMaterial3: true, + ).copyWith( + inputDecorationTheme: _defaultInputDecorationTheme, + listTileTheme: _defaultListTileTheme, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); +} diff --git a/lib/util.dart b/lib/util.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/util.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pubspec.lock b/pubspec.lock index 09ea6a0..1157538 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -433,6 +433,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: "37a15576f5a0bfd5555b613cf20ea3bd379607cf88d457374a16032f4e942174" + url: "https://pub.dev" + source: hosted + version: "1.5.4" edge_detection: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0f94483..c50f2f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,7 @@ dependencies: flutter_staggered_grid_view: ^0.6.2 responsive_builder: ^0.4.3 open_filex: ^4.3.2 + dynamic_color: ^1.5.4 dev_dependencies: integration_test: From e68e3af713c10217f408f33df5ee30bfb95c6398 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 24 Jan 2023 00:38:37 +0100 Subject: [PATCH 08/25] WIP - Added document search, restructured navigation --- .../widgets/material/search/m3_search.dart | 601 ++++++++++++++++++ .../material/search/m3_search_bar.dart | 81 +++ .../cubit/document_search_cubit.dart | 32 +- .../cubit/document_search_state.dart | 9 - .../cubit/document_search_state.g.dart | 21 + .../document_search_delegate.dart | 188 ++++-- .../view/document_search_app_bar.dart | 49 ++ .../documents/view/pages/documents_page.dart | 51 +- lib/features/home/view/home_page.dart | 46 +- lib/features/inbox/view/pages/inbox_page.dart | 1 + lib/l10n/intl_cs.arb | 3 +- lib/l10n/intl_de.arb | 3 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_tr.arb | 3 +- lib/theme.dart | 5 +- 15 files changed, 970 insertions(+), 126 deletions(-) create mode 100644 lib/core/widgets/material/search/m3_search.dart create mode 100644 lib/core/widgets/material/search/m3_search_bar.dart create mode 100644 lib/features/document_search/cubit/document_search_state.g.dart create mode 100644 lib/features/document_search/view/document_search_app_bar.dart diff --git a/lib/core/widgets/material/search/m3_search.dart b/lib/core/widgets/material/search/m3_search.dart new file mode 100644 index 0000000..4be3600 --- /dev/null +++ b/lib/core/widgets/material/search/m3_search.dart @@ -0,0 +1,601 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// Shows a full screen search page and returns the search result selected by +/// the user when the page is closed. +/// +/// The search page consists of an app bar with a search field and a body which +/// can either show suggested search queries or the search results. +/// +/// The appearance of the search page is determined by the provided +/// `delegate`. The initial query string is given by `query`, which defaults +/// to the empty string. When `query` is set to null, `delegate.query` will +/// be used as the initial query. +/// +/// This method returns the selected search result, which can be set in the +/// [SearchDelegate.close] call. If the search page is closed with the system +/// back button, it returns null. +/// +/// A given [SearchDelegate] can only be associated with one active [showMaterial3Search] +/// call. Call [SearchDelegate.close] before re-using the same delegate instance +/// for another [showMaterial3Search] call. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// search page to the [Navigator] furthest from or nearest to the given +/// `context`. By default, `useRootNavigator` is `false` and the search page +/// route created by this method is pushed to the nearest navigator to the +/// given `context`. It can not be `null`. +/// +/// The transition to the search page triggered by this method looks best if the +/// screen triggering the transition contains an [AppBar] at the top and the +/// transition is called from an [IconButton] that's part of [AppBar.actions]. +/// The animation provided by [SearchDelegate.transitionAnimation] can be used +/// to trigger additional animations in the underlying page while the search +/// page fades in or out. This is commonly used to animate an [AnimatedIcon] in +/// the [AppBar.leading] position e.g. from the hamburger menu to the back arrow +/// used to exit the search page. +/// +/// ## Handling emojis and other complex characters +/// {@macro flutter.widgets.EditableText.onChanged} +/// +/// See also: +/// +/// * [SearchDelegate] to define the content of the search page. +Future showMaterial3Search({ + required BuildContext context, + required SearchDelegate delegate, + String? query = '', + bool useRootNavigator = false, +}) { + delegate.query = query ?? delegate.query; + delegate._currentBody = _SearchBody.suggestions; + return Navigator.of(context, rootNavigator: useRootNavigator) + .push(_SearchPageRoute( + delegate: delegate, + )); +} + +/// Delegate for [showMaterial3Search] to define the content of the search page. +/// +/// The search page always shows an [AppBar] at the top where users can +/// enter their search queries. The buttons shown before and after the search +/// query text field can be customized via [SearchDelegate.buildLeading] +/// and [SearchDelegate.buildActions]. Additionally, a widget can be placed +/// across the bottom of the [AppBar] via [SearchDelegate.buildBottom]. +/// +/// The body below the [AppBar] can either show suggested queries (returned by +/// [SearchDelegate.buildSuggestions]) or - once the user submits a search - the +/// results of the search as returned by [SearchDelegate.buildResults]. +/// +/// [SearchDelegate.query] always contains the current query entered by the user +/// and should be used to build the suggestions and results. +/// +/// The results can be brought on screen by calling [SearchDelegate.showResults] +/// and you can go back to showing the suggestions by calling +/// [SearchDelegate.showSuggestions]. +/// +/// Once the user has selected a search result, [SearchDelegate.close] should be +/// called to remove the search page from the top of the navigation stack and +/// to notify the caller of [showMaterial3Search] about the selected search result. +/// +/// A given [SearchDelegate] can only be associated with one active [showMaterial3Search] +/// call. Call [SearchDelegate.close] before re-using the same delegate instance +/// for another [showMaterial3Search] call. +/// +/// ## Handling emojis and other complex characters +/// {@macro flutter.widgets.EditableText.onChanged} +abstract class SearchDelegate { + /// Constructor to be called by subclasses which may specify + /// [searchFieldLabel], either [searchFieldStyle] or [searchFieldDecorationTheme], + /// [keyboardType] and/or [textInputAction]. Only one of [searchFieldLabel] + /// and [searchFieldDecorationTheme] may be non-null. + /// + /// {@tool snippet} + /// ```dart + /// class CustomSearchHintDelegate extends SearchDelegate { + /// CustomSearchHintDelegate({ + /// required String hintText, + /// }) : super( + /// searchFieldLabel: hintText, + /// keyboardType: TextInputType.text, + /// textInputAction: TextInputAction.search, + /// ); + /// + /// @override + /// Widget buildLeading(BuildContext context) => const Text('leading'); + /// + /// @override + /// PreferredSizeWidget buildBottom(BuildContext context) { + /// return const PreferredSize( + /// preferredSize: Size.fromHeight(56.0), + /// child: Text('bottom')); + /// } + /// + /// @override + /// Widget buildSuggestions(BuildContext context) => const Text('suggestions'); + /// + /// @override + /// Widget buildResults(BuildContext context) => const Text('results'); + /// + /// @override + /// List buildActions(BuildContext context) => []; + /// } + /// ``` + /// {@end-tool} + SearchDelegate({ + this.searchFieldLabel, + this.searchFieldStyle, + this.searchFieldDecorationTheme, + this.keyboardType, + this.textInputAction = TextInputAction.search, + }) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null); + + /// Suggestions shown in the body of the search page while the user types a + /// query into the search field. + /// + /// The delegate method is called whenever the content of [query] changes. + /// The suggestions should be based on the current [query] string. If the query + /// string is empty, it is good practice to show suggested queries based on + /// past queries or the current context. + /// + /// Usually, this method will return a [ListView] with one [ListTile] per + /// suggestion. When [ListTile.onTap] is called, [query] should be updated + /// with the corresponding suggestion and the results page should be shown + /// by calling [showResults]. + Widget buildSuggestions(BuildContext context); + + /// The results shown after the user submits a search from the search page. + /// + /// The current value of [query] can be used to determine what the user + /// searched for. + /// + /// This method might be applied more than once to the same query. + /// If your [buildResults] method is computationally expensive, you may want + /// to cache the search results for one or more queries. + /// + /// Typically, this method returns a [ListView] with the search results. + /// When the user taps on a particular search result, [close] should be called + /// with the selected result as argument. This will close the search page and + /// communicate the result back to the initial caller of [showMaterial3Search]. + Widget buildResults(BuildContext context); + + /// A widget to display before the current query in the [AppBar]. + /// + /// Typically an [IconButton] configured with a [BackButtonIcon] that exits + /// the search with [close]. One can also use an [AnimatedIcon] driven by + /// [transitionAnimation], which animates from e.g. a hamburger menu to the + /// back button as the search overlay fades in. + /// + /// Returns null if no widget should be shown. + /// + /// See also: + /// + /// * [AppBar.leading], the intended use for the return value of this method. + Widget? buildLeading(BuildContext context); + + /// Widgets to display after the search query in the [AppBar]. + /// + /// If the [query] is not empty, this should typically contain a button to + /// clear the query and show the suggestions again (via [showSuggestions]) if + /// the results are currently shown. + /// + /// Returns null if no widget should be shown. + /// + /// See also: + /// + /// * [AppBar.actions], the intended use for the return value of this method. + List? buildActions(BuildContext context); + + /// Widget to display across the bottom of the [AppBar]. + /// + /// Returns null by default, i.e. a bottom widget is not included. + /// + /// See also: + /// + /// * [AppBar.bottom], the intended use for the return value of this method. + /// + PreferredSizeWidget? buildBottom(BuildContext context) => null; + + /// The theme used to configure the search page. + /// + /// The returned [ThemeData] will be used to wrap the entire search page, + /// so it can be used to configure any of its components with the appropriate + /// theme properties. + /// + /// Unless overridden, the default theme will configure the AppBar containing + /// the search input text field with a white background and black text on light + /// themes. For dark themes the default is a dark grey background with light + /// color text. + /// + /// See also: + /// + /// * [AppBarTheme], which configures the AppBar's appearance. + /// * [InputDecorationTheme], which configures the appearance of the search + /// text field. + ThemeData appBarTheme(BuildContext context) { + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + return theme.copyWith( + appBarTheme: AppBarTheme( + brightness: colorScheme.brightness, + backgroundColor: colorScheme.brightness == Brightness.dark + ? Colors.grey[900] + : Colors.white, + iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), + textTheme: theme.textTheme, + ), + inputDecorationTheme: searchFieldDecorationTheme ?? + InputDecorationTheme( + hintStyle: searchFieldStyle ?? theme.inputDecorationTheme.hintStyle, + border: InputBorder.none, + ), + ); + } + + /// The current query string shown in the [AppBar]. + /// + /// The user manipulates this string via the keyboard. + /// + /// If the user taps on a suggestion provided by [buildSuggestions] this + /// string should be updated to that suggestion via the setter. + String get query => _queryTextController.text; + + /// Changes the current query string. + /// + /// Setting the query string programmatically moves the cursor to the end of the text field. + set query(String value) { + assert(query != null); + _queryTextController.text = value; + if (_queryTextController.text.isNotEmpty) { + _queryTextController.selection = TextSelection.fromPosition( + TextPosition(offset: _queryTextController.text.length)); + } + } + + /// Transition from the suggestions returned by [buildSuggestions] to the + /// [query] results returned by [buildResults]. + /// + /// If the user taps on a suggestion provided by [buildSuggestions] the + /// screen should typically transition to the page showing the search + /// results for the suggested query. This transition can be triggered + /// by calling this method. + /// + /// See also: + /// + /// * [showSuggestions] to show the search suggestions again. + void showResults(BuildContext context) { + _focusNode?.unfocus(); + _currentBody = _SearchBody.results; + } + + /// Transition from showing the results returned by [buildResults] to showing + /// the suggestions returned by [buildSuggestions]. + /// + /// Calling this method will also put the input focus back into the search + /// field of the [AppBar]. + /// + /// If the results are currently shown this method can be used to go back + /// to showing the search suggestions. + /// + /// See also: + /// + /// * [showResults] to show the search results. + void showSuggestions(BuildContext context) { + assert(_focusNode != null, + '_focusNode must be set by route before showSuggestions is called.'); + _focusNode!.requestFocus(); + _currentBody = _SearchBody.suggestions; + } + + /// Closes the search page and returns to the underlying route. + /// + /// The value provided for `result` is used as the return value of the call + /// to [showMaterial3Search] that launched the search initially. + void close(BuildContext context, T result) { + _currentBody = null; + _focusNode?.unfocus(); + Navigator.of(context) + ..popUntil((Route route) => route == _route) + ..pop(result); + } + + /// The hint text that is shown in the search field when it is empty. + /// + /// If this value is set to null, the value of + /// `MaterialLocalizations.of(context).searchFieldLabel` will be used instead. + final String? searchFieldLabel; + + /// The style of the [searchFieldLabel]. + /// + /// If this value is set to null, the value of the ambient [Theme]'s + /// [InputDecorationTheme.hintStyle] will be used instead. + /// + /// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can + /// be non-null. + final TextStyle? searchFieldStyle; + + /// The [InputDecorationTheme] used to configure the search field's visuals. + /// + /// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can + /// be non-null. + final InputDecorationTheme? searchFieldDecorationTheme; + + /// The type of action button to use for the keyboard. + /// + /// Defaults to the default value specified in [TextField]. + final TextInputType? keyboardType; + + /// The text input action configuring the soft keyboard to a particular action + /// button. + /// + /// Defaults to [TextInputAction.search]. + final TextInputAction textInputAction; + + /// [Animation] triggered when the search pages fades in or out. + /// + /// This animation is commonly used to animate [AnimatedIcon]s of + /// [IconButton]s returned by [buildLeading] or [buildActions]. It can also be + /// used to animate [IconButton]s contained within the route below the search + /// page. + Animation get transitionAnimation => _proxyAnimation; + + // The focus node to use for manipulating focus on the search page. This is + // managed, owned, and set by the _SearchPageRoute using this delegate. + FocusNode? _focusNode; + + final TextEditingController _queryTextController = TextEditingController(); + + final ProxyAnimation _proxyAnimation = + ProxyAnimation(kAlwaysDismissedAnimation); + + final ValueNotifier<_SearchBody?> _currentBodyNotifier = + ValueNotifier<_SearchBody?>(null); + + _SearchBody? get _currentBody => _currentBodyNotifier.value; + set _currentBody(_SearchBody? value) { + _currentBodyNotifier.value = value; + } + + _SearchPageRoute? _route; +} + +/// Describes the body that is currently shown under the [AppBar] in the +/// search page. +enum _SearchBody { + /// Suggested queries are shown in the body. + /// + /// The suggested queries are generated by [SearchDelegate.buildSuggestions]. + suggestions, + + /// Search results are currently shown in the body. + /// + /// The search results are generated by [SearchDelegate.buildResults]. + results, +} + +class _SearchPageRoute extends PageRoute { + _SearchPageRoute({ + required this.delegate, + }) : assert(delegate != null) { + assert( + delegate._route == null, + 'The ${delegate.runtimeType} instance is currently used by another active ' + 'search. Please close that search by calling close() on the SearchDelegate ' + 'before opening another search with the same delegate instance.', + ); + delegate._route = this; + } + + final SearchDelegate delegate; + + @override + Color? get barrierColor => null; + + @override + String? get barrierLabel => null; + + @override + Duration get transitionDuration => const Duration(milliseconds: 300); + + @override + bool get maintainState => false; + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition( + opacity: animation, + child: child, + ); + } + + @override + Animation createAnimation() { + final Animation animation = super.createAnimation(); + delegate._proxyAnimation.parent = animation; + return animation; + } + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return _SearchPage( + delegate: delegate, + animation: animation, + ); + } + + @override + void didComplete(T? result) { + super.didComplete(result); + assert(delegate._route == this); + delegate._route = null; + delegate._currentBody = null; + } +} + +class _SearchPage extends StatefulWidget { + const _SearchPage({ + required this.delegate, + required this.animation, + }); + + final SearchDelegate delegate; + final Animation animation; + + @override + State createState() => _SearchPageState(); +} + +class _SearchPageState extends State<_SearchPage> { + // This node is owned, but not hosted by, the search page. Hosting is done by + // the text field. + FocusNode focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + widget.delegate._queryTextController.addListener(_onQueryChanged); + widget.animation.addStatusListener(_onAnimationStatusChanged); + widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged); + focusNode.addListener(_onFocusChanged); + widget.delegate._focusNode = focusNode; + } + + @override + void dispose() { + super.dispose(); + widget.delegate._queryTextController.removeListener(_onQueryChanged); + widget.animation.removeStatusListener(_onAnimationStatusChanged); + widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged); + widget.delegate._focusNode = null; + focusNode.dispose(); + } + + void _onAnimationStatusChanged(AnimationStatus status) { + if (status != AnimationStatus.completed) { + return; + } + widget.animation.removeStatusListener(_onAnimationStatusChanged); + if (widget.delegate._currentBody == _SearchBody.suggestions) { + focusNode.requestFocus(); + } + } + + @override + void didUpdateWidget(_SearchPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.delegate != oldWidget.delegate) { + oldWidget.delegate._queryTextController.removeListener(_onQueryChanged); + widget.delegate._queryTextController.addListener(_onQueryChanged); + oldWidget.delegate._currentBodyNotifier + .removeListener(_onSearchBodyChanged); + widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged); + oldWidget.delegate._focusNode = null; + widget.delegate._focusNode = focusNode; + } + } + + void _onFocusChanged() { + if (focusNode.hasFocus && + widget.delegate._currentBody != _SearchBody.suggestions) { + widget.delegate.showSuggestions(context); + } + } + + void _onQueryChanged() { + setState(() { + // rebuild ourselves because query changed. + }); + } + + void _onSearchBodyChanged() { + setState(() { + // rebuild ourselves because search body changed. + }); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + final ThemeData theme = widget.delegate.appBarTheme(context); + final String searchFieldLabel = widget.delegate.searchFieldLabel ?? + MaterialLocalizations.of(context).searchFieldLabel; + Widget? body; + switch (widget.delegate._currentBody) { + case _SearchBody.suggestions: + body = KeyedSubtree( + key: const ValueKey<_SearchBody>(_SearchBody.suggestions), + child: widget.delegate.buildSuggestions(context), + ); + break; + case _SearchBody.results: + body = KeyedSubtree( + key: const ValueKey<_SearchBody>(_SearchBody.results), + child: widget.delegate.buildResults(context), + ); + break; + case null: + break; + } + + late final String routeName; + switch (theme.platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + routeName = ''; + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + routeName = searchFieldLabel; + } + + return Semantics( + explicitChildNodes: true, + scopesRoute: true, + namesRoute: true, + label: routeName, + child: Theme( + data: theme, + child: Scaffold( + appBar: AppBar( + toolbarHeight: 72, + leading: widget.delegate.buildLeading(context), + title: TextField( + controller: widget.delegate._queryTextController, + focusNode: focusNode, + style: widget.delegate.searchFieldStyle ?? + theme.textTheme.titleLarge, + textInputAction: widget.delegate.textInputAction, + keyboardType: widget.delegate.keyboardType, + onSubmitted: (String _) { + widget.delegate.showResults(context); + }, + decoration: InputDecoration(hintText: searchFieldLabel), + ), + actions: widget.delegate.buildActions(context), + bottom: widget.delegate.buildBottom(context), + ), + body: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: body, + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/material/search/m3_search_bar.dart b/lib/core/widgets/material/search/m3_search_bar.dart new file mode 100644 index 0000000..1ec56ce --- /dev/null +++ b/lib/core/widgets/material/search/m3_search_bar.dart @@ -0,0 +1,81 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class SearchBar extends StatelessWidget { + const SearchBar({ + Key? key, + this.height = 56, + required this.leadingIcon, + this.trailingIcon, + required this.supportingText, + required this.onTap, + }) : super(key: key); + + final double height; + double get effectiveHeight { + return max(height, 48); + } + + final VoidCallback onTap; + final Widget leadingIcon; + final Widget? trailingIcon; + + final String supportingText; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final TextTheme textTheme = Theme.of(context).textTheme; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + constraints: const BoxConstraints(minWidth: 360, maxWidth: 720), + width: double.infinity, + height: effectiveHeight, + child: Material( + elevation: 3, + color: colorScheme.surface, + shadowColor: colorScheme.shadow, + surfaceTintColor: colorScheme.surfaceTint, + borderRadius: BorderRadius.circular(effectiveHeight / 2), + child: InkWell( + onTap: () {}, + borderRadius: BorderRadius.circular(effectiveHeight / 2), + highlightColor: Colors.transparent, + splashFactory: InkRipple.splashFactory, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row(children: [ + leadingIcon, + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: TextField( + cursorColor: colorScheme.primary, + style: textTheme.bodyLarge, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 8), + hintText: supportingText, + hintStyle: textTheme.bodyLarge?.apply( + color: colorScheme.onSurfaceVariant, + ), + ), + onTap: onTap, + ), + ), + ), + if (trailingIcon != null) trailingIcon!, + ]), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart index bd81066..26d39bc 100644 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -1,29 +1,47 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; +import 'package:collection/collection.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_api/src/modules/documents_api/paperless_documents_api.dart'; import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; import 'document_search_state.dart'; class DocumentSearchCubit extends HydratedCubit with DocumentsPagingMixin { + //// DocumentSearchCubit(this.api) : super(const DocumentSearchState()); @override final PaperlessDocumentsApi api; + /// + /// Requests results based on [query] and adds [query] to the + /// search history, removing old occurrences and trimming the list to + /// the last 5 searches. + /// Future updateResults(String query) async { await updateFilter( filter: state.filter.copyWith(query: TextQuery.titleAndContent(query)), ); - emit(state.copyWith(searchHistory: [query, ...state.searchHistory])); + emit( + state.copyWith( + searchHistory: [ + query, + ...state.searchHistory.where((element) => element != query) + ].take(5).toList(), + ), + ); } - Future updateSuggestions(String query) async { - final suggestions = await api.autocomplete(query); - emit(state.copyWith(suggestions: suggestions)); + void removeHistoryEntry(String suggestion) { + emit(state.copyWith( + searchHistory: state.searchHistory + .whereNot((element) => element == suggestion) + .toList(), + )); + } + + Future> findSuggestions(String query) { + return api.autocomplete(query); } @override diff --git a/lib/features/document_search/cubit/document_search_state.dart b/lib/features/document_search/cubit/document_search_state.dart index a6b0be7..4286fb5 100644 --- a/lib/features/document_search/cubit/document_search_state.dart +++ b/lib/features/document_search/cubit/document_search_state.dart @@ -5,18 +5,13 @@ import 'package:paperless_mobile/features/paged_document_view/model/documents_pa part 'document_search_state.g.dart'; - - @JsonSerializable(ignoreUnannotated: true) class DocumentSearchState extends DocumentsPagedState { @JsonKey() final List searchHistory; - final List suggestions; - const DocumentSearchState({ this.searchHistory = const [], - this.suggestions = const [], super.filter, super.hasLoaded, super.isLoading, @@ -30,7 +25,6 @@ class DocumentSearchState extends DocumentsPagedState { filter, value, searchHistory, - suggestions, ]; @override @@ -62,7 +56,6 @@ class DocumentSearchState extends DocumentsPagedState { hasLoaded: hasLoaded ?? this.hasLoaded, isLoading: isLoading ?? this.isLoading, searchHistory: searchHistory ?? this.searchHistory, - suggestions: suggestions ?? this.suggestions, ); } @@ -71,5 +64,3 @@ class DocumentSearchState extends DocumentsPagedState { Map toJson() => _$DocumentSearchStateToJson(this); } - -class diff --git a/lib/features/document_search/cubit/document_search_state.g.dart b/lib/features/document_search/cubit/document_search_state.g.dart new file mode 100644 index 0000000..6d1bb68 --- /dev/null +++ b/lib/features/document_search/cubit/document_search_state.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'document_search_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DocumentSearchState _$DocumentSearchStateFromJson(Map json) => + DocumentSearchState( + searchHistory: (json['searchHistory'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + ); + +Map _$DocumentSearchStateToJson( + DocumentSearchState instance) => + { + 'searchHistory': instance.searchHistory, + }; diff --git a/lib/features/document_search/document_search_delegate.dart b/lib/features/document_search/document_search_delegate.dart index d70af8b..6e25a6d 100644 --- a/lib/features/document_search/document_search_delegate.dart +++ b/lib/features/document_search/document_search_delegate.dart @@ -3,15 +3,21 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; -import 'package:provider/provider.dart'; -class DocumentSearchDelegate extends SearchDelegate { - DocumentSearchDelegate({ +import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart' + as m3; +import 'package:paperless_mobile/generated/l10n.dart'; + +class DocumentSearchDelegate extends m3.SearchDelegate { + final DocumentSearchCubit bloc; + DocumentSearchDelegate( + this.bloc, { required String hintText, required super.searchFieldStyle, }) : super( @@ -23,60 +29,141 @@ class DocumentSearchDelegate extends SearchDelegate { @override Widget buildLeading(BuildContext context) => const BackButton(); + @override + PreferredSizeWidget buildBottom(BuildContext context) => PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Divider( + color: Theme.of(context).colorScheme.outline, + height: 1, + ), + ); @override Widget buildSuggestions(BuildContext context) { - BlocBuilder( + return BlocBuilder( + bloc: bloc, builder: (context, state) { - if (!state.hasLoaded && state.isLoading) { - return const DocumentsListLoadingWidget(); - } - return ListView.builder(itemBuilder: (context, index) => ListTile( - title: Text(snapshot.data![index]), - onTap: () { - query = snapshot.data![index]; - super.showResults(context); - }, - ),); - }, - ) - return FutureBuilder( - future: context.read().autocomplete(query), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), + if (query.isEmpty) { + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Text( + "History", //TODO: INTL + style: Theme.of(context).textTheme.labelMedium, + ).padded(16), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final label = state.searchHistory[index]; + return ListTile( + leading: const Icon(Icons.history), + title: Text(label), + onTap: () => _onSuggestionSelected( + context, + label, + ), + onLongPress: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(label), + content: Text( + S.of(context).documentSearchPageRemoveFromHistory, + ), + actions: [ + TextButton( + child: Text( + S.of(context).genericActionCancelLabel, + ), + onPressed: () => Navigator.pop(context), + ), + TextButton( + child: Text( + S.of(context).genericActionDeleteLabel, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + onPressed: () { + bloc.removeHistoryEntry(label); + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + }, + childCount: state.searchHistory.length, + ), + ), + ], ); } - return ListView.builder( - itemCount: snapshot.data!.length, - itemBuilder: (context, index) => ListTile( - title: Text(snapshot.data![index]), - onTap: () { - query = snapshot.data![index]; - super.showResults(context); - }, - ), - ); + return FutureBuilder>( + future: bloc.findSuggestions(query), + builder: (context, snapshot) { + final historyMatches = state.searchHistory + .where((e) => e.startsWith(query)) + .toList(); + final serverSuggestions = (snapshot.data ?? []) + ..removeWhere((e) => historyMatches.contains(e)); + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Text( + "Results", //TODO: INTL + style: Theme.of(context).textTheme.labelMedium, + ).padded(), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => ListTile( + title: Text(historyMatches[index]), + leading: const Icon(Icons.history), + onTap: () => _onSuggestionSelected( + context, + historyMatches[index], + ), + ), + childCount: historyMatches.length, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => ListTile( + title: Text(serverSuggestions[index]), + leading: const Icon(Icons.search), + onTap: () => _onSuggestionSelected( + context, snapshot.data![index]), + ), + childCount: serverSuggestions.length, + ), + ), + ], + ); + }); }, ); } + void _onSuggestionSelected(BuildContext context, String suggestion) { + query = suggestion; + bloc.updateResults(query); + super.showResults(context); + } + @override Widget buildResults(BuildContext context) { - return FutureBuilder( - future: context - .read() - .findAll(DocumentFilter(query: TextQuery.titleAndContent(query))), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); + return BlocBuilder( + bloc: bloc, + builder: (context, state) { + if (!state.hasLoaded && state.isLoading) { + return const DocumentsListLoadingWidget(); } - final documents = snapshot.data!.results; return ListView.builder( + itemCount: state.documents.length, itemBuilder: (context, index) => DocumentListItem( - document: documents[index], + document: state.documents[index], onTap: (document) { Navigator.push( context, @@ -102,5 +189,18 @@ class DocumentSearchDelegate extends SearchDelegate { } @override - List buildActions(BuildContext context) => []; + List? buildActions(BuildContext context) { + return [ + IconButton( + icon: Icon( + Icons.clear, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ).paddedSymmetrically(horizontal: 16), + onPressed: () { + query = ''; + super.showSuggestions(context); + }, + ), + ]; + } } diff --git a/lib/features/document_search/view/document_search_app_bar.dart b/lib/features/document_search/view/document_search_app_bar.dart new file mode 100644 index 0000000..2312f3a --- /dev/null +++ b/lib/features/document_search/view/document_search_app_bar.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; +import 'package:paperless_mobile/features/document_search/document_search_delegate.dart'; +import 'package:provider/provider.dart'; + +class DocumentSearchAppBar extends StatelessWidget { + const DocumentSearchAppBar({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return TextField( + onTap: () => showMaterial3Search( + context: context, + delegate: DocumentSearchDelegate( + DocumentSearchCubit(context.read()), + searchFieldStyle: Theme.of(context).textTheme.bodyLarge, + hintText: "Search documents", + ), + ), + readOnly: true, + decoration: InputDecoration( + hintText: "Search documents", + hintStyle: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), + filled: true, + fillColor: Theme.of(context).colorScheme.surfaceVariant, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(56), + borderSide: BorderSide.none, + ), + prefixIcon: IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ), + constraints: const BoxConstraints(maxHeight: 48), + ), + // title: Text( + // "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})", + // ), + ); + } +} diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 75c8da3..2b78798 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -5,10 +5,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/document_search_delegate.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_app_bar.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; @@ -143,48 +146,14 @@ class _DocumentsPageState extends State { ), appBar: PreferredSize( preferredSize: const Size.fromHeight( - kToolbarHeight + linearProgressIndicatorHeight, + kToolbarHeight, ), child: BlocBuilder( builder: (context, state) { if (state.selection.isEmpty) { return AppBar( - title: TextField( - onTap: () => showSearch( - context: context, - delegate: DocumentSearchDelegate( - searchFieldStyle: - Theme.of(context).textTheme.bodyLarge, - hintText: "Search your documents", - ), - ), - readOnly: true, - decoration: InputDecoration( - hintText: "Search your documents", - hintStyle: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant), - filled: true, - fillColor: Theme.of(context).colorScheme.surface, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(32), - borderSide: BorderSide.none, - ), - prefixIcon: IconButton( - icon: const Icon(Icons.menu), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ), - ), - ), - // title: Text( - // "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})", - // ), + automaticallyImplyLeading: false, + title: const DocumentSearchAppBar(), actions: [ const SortDocumentsButton(), BlocBuilder { ), ), ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight( - linearProgressIndicatorHeight), - child: state.isLoading && state.hasLoaded - ? const LinearProgressIndicator() - : const SizedBox(height: 4.0), - ), - automaticallyImplyLeading: false, ); } else { return AppBar( diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index fafd6bd..a49985e 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -21,11 +21,14 @@ import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/route_description.dart'; import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; +import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; +import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; +import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -171,22 +174,22 @@ class _HomePageState extends State { ), label: S.of(context).bottomNavLabelsPageLabel, ), - // RouteDescription( - // icon: const Icon(Icons.inbox_outlined), - // selectedIcon: Icon( - // Icons.inbox, - // color: Theme.of(context).colorScheme.primary, - // ), - // label: S.of(context).bottomNavInboxPageLabel, - // ), - // RouteDescription( - // icon: const Icon(Icons.settings_outlined), - // selectedIcon: Icon( - // Icons.settings, - // color: Theme.of(context).colorScheme.primary, - // ), - // label: S.of(context).appDrawerSettingsLabel, - // ), + RouteDescription( + icon: const Icon(Icons.inbox_outlined), + selectedIcon: Icon( + Icons.inbox, + color: Theme.of(context).colorScheme.primary, + ), + label: S.of(context).bottomNavInboxPageLabel, + ), + RouteDescription( + icon: const Icon(Icons.settings_outlined), + selectedIcon: Icon( + Icons.settings, + color: Theme.of(context).colorScheme.primary, + ), + label: S.of(context).appDrawerSettingsLabel, + ), ]; final routes = [ MultiBlocProvider( @@ -210,6 +213,16 @@ class _HomePageState extends State { child: const ScannerPage(), ), const LabelsPage(), + BlocProvider( + create: (context) => InboxCubit( + context.read(), + context.read(), + context.read(), + context.read(), + ), + child: const InboxPage(), + ), + const SettingsPage(), ]; return MultiBlocListener( listeners: [ @@ -257,6 +270,7 @@ class _HomePageState extends State { } return Scaffold( bottomNavigationBar: NavigationBar( + labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, elevation: 4.0, selectedIndex: _currentIndex, onDestinationSelected: _onNavigationChanged, diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index c464e53..55bacb5 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -30,6 +30,7 @@ class _InboxPageState extends State { @override void initState() { super.initState(); + context.read().initializeInbox(); _scrollController.addListener(_listenForLoadNewData); } diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 98806d0..5553593 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -616,5 +616,6 @@ "colorSchemeOptionClassic": "Classic", "colorSchemeOptionDznamic": "Dynamic", "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", - "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation.", + "documentSearchPageRemoveFromHistory": "Remove from search history?" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index e32b4e3..dd50fb4 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -616,5 +616,6 @@ "colorSchemeOptionClassic": "Classic", "colorSchemeOptionDznamic": "Dynamic", "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", - "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation.", + "documentSearchPageRemoveFromHistory": "Remove from search history?" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ba3400d..04d7266 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -616,5 +616,6 @@ "colorSchemeOptionClassic": "Classic", "colorSchemeOptionDynamic": "Dynamic", "settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", - "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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." + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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.", + "documentSearchPageRemoveFromHistory": "Remove from search history?" } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index f6a1e0b..230e2d1 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -616,5 +616,6 @@ "colorSchemeOptionClassic": "Classic", "colorSchemeOptionDznamic": "Dynamic", "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", - "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation." + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation.", + "documentSearchPageRemoveFromHistory": "Remove from search history?" } \ No newline at end of file diff --git a/lib/theme.dart b/lib/theme.dart index 172978b..27ec7cb 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -26,7 +26,7 @@ ThemeData buildTheme({ final classicScheme = ColorScheme.fromSeed( seedColor: _classicThemeColorSeed, brightness: brightness, - ); + ).harmonized(); late ColorScheme colorScheme; switch (preferredColorScheme) { case ColorSchemeOption.classic: @@ -43,5 +43,8 @@ ThemeData buildTheme({ inputDecorationTheme: _defaultInputDecorationTheme, listTileTheme: _defaultListTileTheme, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + appBarTheme: AppBarTheme( + scrolledUnderElevation: 0, + ), ); } From a7b980ae711fd791259ab7bc381538b7fc1e8954 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 24 Jan 2023 22:03:34 +0100 Subject: [PATCH 09/25] Changed loading animation, directly accessible pages and updated translations --- .../widgets/material/search/m3_search.dart | 5 ++-- .../document_search_delegate.dart | 4 +-- ..._app_bar.dart => document_search_bar.dart} | 6 ++--- .../documents/view/pages/documents_page.dart | 22 +++++++++++++--- .../widgets/list/adaptive_documents_view.dart | 4 ++- .../widgets/new_items_loading_widget.dart | 3 ++- lib/features/home/view/home_page.dart | 20 +++++++------- lib/features/inbox/view/pages/inbox_page.dart | 12 --------- lib/l10n/intl_cs.arb | 24 ++++++++++++----- lib/l10n/intl_de.arb | 26 +++++++++++++------ lib/l10n/intl_en.arb | 24 ++++++++++++----- lib/l10n/intl_tr.arb | 24 ++++++++++++----- 12 files changed, 110 insertions(+), 64 deletions(-) rename lib/features/document_search/view/{document_search_app_bar.dart => document_search_bar.dart} (92%) diff --git a/lib/core/widgets/material/search/m3_search.dart b/lib/core/widgets/material/search/m3_search.dart index 4be3600..43572dc 100644 --- a/lib/core/widgets/material/search/m3_search.dart +++ b/lib/core/widgets/material/search/m3_search.dart @@ -1,10 +1,9 @@ +//TODO: REMOVE THIS WHEN NATIVE MATERIAL FLUTTER SEARCH IS RELEASED // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; /// Shows a full screen search page and returns the search result selected by /// the user when the page is closed. @@ -381,7 +380,7 @@ enum _SearchBody { class _SearchPageRoute extends PageRoute { _SearchPageRoute({ required this.delegate, - }) : assert(delegate != null) { + }) { assert( delegate._route == null, 'The ${delegate.runtimeType} instance is currently used by another active ' diff --git a/lib/features/document_search/document_search_delegate.dart b/lib/features/document_search/document_search_delegate.dart index 6e25a6d..17e8e3b 100644 --- a/lib/features/document_search/document_search_delegate.dart +++ b/lib/features/document_search/document_search_delegate.dart @@ -47,7 +47,7 @@ class DocumentSearchDelegate extends m3.SearchDelegate { slivers: [ SliverToBoxAdapter( child: Text( - "History", //TODO: INTL + S.of(context).documentSearchHistory, style: Theme.of(context).textTheme.labelMedium, ).padded(16), ), @@ -111,7 +111,7 @@ class DocumentSearchDelegate extends m3.SearchDelegate { slivers: [ SliverToBoxAdapter( child: Text( - "Results", //TODO: INTL + S.of(context).documentSearchResults, style: Theme.of(context).textTheme.labelMedium, ).padded(), ), diff --git a/lib/features/document_search/view/document_search_app_bar.dart b/lib/features/document_search/view/document_search_bar.dart similarity index 92% rename from lib/features/document_search/view/document_search_app_bar.dart rename to lib/features/document_search/view/document_search_bar.dart index 2312f3a..2f70365 100644 --- a/lib/features/document_search/view/document_search_app_bar.dart +++ b/lib/features/document_search/view/document_search_bar.dart @@ -4,8 +4,8 @@ import 'package:paperless_mobile/features/document_search/cubit/document_search_ import 'package:paperless_mobile/features/document_search/document_search_delegate.dart'; import 'package:provider/provider.dart'; -class DocumentSearchAppBar extends StatelessWidget { - const DocumentSearchAppBar({ +class DocumentSearchBar extends StatelessWidget { + const DocumentSearchBar({ super.key, }); @@ -34,7 +34,7 @@ class DocumentSearchAppBar extends StatelessWidget { borderSide: BorderSide.none, ), prefixIcon: IconButton( - icon: const Icon(Icons.menu), + icon: const Icon(Icons.search), onPressed: () { Scaffold.of(context).openDrawer(); }, diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 2b78798..a2a9f96 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -11,7 +11,7 @@ import 'package:paperless_mobile/features/document_details/bloc/document_details import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/document_search_delegate.dart'; -import 'package:paperless_mobile/features/document_search/view/document_search_app_bar.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; @@ -30,6 +30,7 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_sta import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/constants.dart'; @@ -152,9 +153,24 @@ class _DocumentsPageState extends State { builder: (context, state) { if (state.selection.isEmpty) { return AppBar( - automaticallyImplyLeading: false, - title: const DocumentSearchAppBar(), + automaticallyImplyLeading: true, + title: Text(S.of(context).documentsPageTitle + + " (${formatMaxCount(state.documents.length)})"), actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + showMaterial3Search( + context: context, + delegate: DocumentSearchDelegate( + DocumentSearchCubit(context.read()), + searchFieldStyle: + Theme.of(context).textTheme.bodyLarge, + hintText: "Search documents", + ), + ); + }, + ), const SortDocumentsButton(), BlocBuilder( diff --git a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart index 57ba0f0..1f2b8d8 100644 --- a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart @@ -7,12 +7,12 @@ import 'package:paperless_mobile/features/documents/view/widgets/list/document_l import 'package:paperless_mobile/features/settings/model/view_type.dart'; class AdaptiveDocumentsView extends StatelessWidget { + final DocumentsState state; final ViewType viewType; final Widget beforeItems; final void Function(DocumentModel) onTap; final void Function(DocumentModel) onSelected; final ScrollController scrollController; - final DocumentsState state; final bool hasInternetConnection; final bool isLabelClickable; final void Function(int id)? onTagSelected; @@ -46,6 +46,8 @@ class AdaptiveDocumentsView extends StatelessWidget { slivers: [ SliverToBoxAdapter(child: beforeItems), if (viewType == ViewType.list) _buildListView() else _buildGridView(), + if (state.hasLoaded && state.isLoading) + SliverToBoxAdapter(child: pageLoadingWidget), ], ); } diff --git a/lib/features/documents/view/widgets/new_items_loading_widget.dart b/lib/features/documents/view/widgets/new_items_loading_widget.dart index dfe4553..042f692 100644 --- a/lib/features/documents/view/widgets/new_items_loading_widget.dart +++ b/lib/features/documents/view/widgets/new_items_loading_widget.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; class NewItemsLoadingWidget extends StatelessWidget { const NewItemsLoadingWidget({super.key}); @override Widget build(BuildContext context) { - return const CircularProgressIndicator(); + return Center(child: const CircularProgressIndicator().padded()); } } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index a49985e..d5baa79 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -182,14 +182,14 @@ class _HomePageState extends State { ), label: S.of(context).bottomNavInboxPageLabel, ), - RouteDescription( - icon: const Icon(Icons.settings_outlined), - selectedIcon: Icon( - Icons.settings, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context).appDrawerSettingsLabel, - ), + // RouteDescription( + // icon: const Icon(Icons.settings_outlined), + // selectedIcon: Icon( + // Icons.settings, + // color: Theme.of(context).colorScheme.primary, + // ), + // label: S.of(context).appDrawerSettingsLabel, + // ), ]; final routes = [ MultiBlocProvider( @@ -222,7 +222,7 @@ class _HomePageState extends State { ), child: const InboxPage(), ), - const SettingsPage(), + // const SettingsPage(), ]; return MultiBlocListener( listeners: [ @@ -270,7 +270,7 @@ class _HomePageState extends State { } return Scaffold( bottomNavigationBar: NavigationBar( - labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, elevation: 4.0, selectedIndex: _currentIndex, onDestinationSelected: _onNavigationChanged, diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 55bacb5..1f0245d 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -1,7 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; @@ -14,7 +13,6 @@ import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget. import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/constants.dart'; class InboxPage extends StatefulWidget { const InboxPage({super.key}); @@ -65,10 +63,6 @@ class _InboxPageState extends State { builder: (context, state) { return AppBar( title: Text(S.of(context).bottomNavInboxPageLabel), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), actions: [ if (state.hasLoaded) Align( @@ -89,12 +83,6 @@ class _InboxPageState extends State { ), ).paddedSymmetrically(horizontal: 8) ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight(4), - child: state.isLoading && state.hasLoaded - ? const LinearProgressIndicator() - : const SizedBox(height: _progressBarHeight), - ), ); }, ), diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 5553593..8751050 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -44,6 +44,10 @@ "@bottomNavLabelsPageLabel": {}, "bottomNavScannerPageLabel": "Skener", "@bottomNavScannerPageLabel": {}, + "colorSchemeOptionClassic": "Classic", + "@colorSchemeOptionClassic": {}, + "colorSchemeOptionDynamic": "Dynamic", + "@colorSchemeOptionDynamic": {}, "correspondentFormFieldSearchHintText": "Začni psát...", "@correspondentFormFieldSearchHintText": {}, "deleteViewDialogContentText": "Opravdu chceš tento náhled smazat?", @@ -144,6 +148,12 @@ "@documentScannerPageUploadButtonTooltip": {}, "documentScannerPageUploadFromThisDeviceButtonLabel": "Nahrát jeden dokument z tohoto zařízení", "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, + "documentSearchHistory": "History", + "@documentSearchHistory": {}, + "documentSearchPageRemoveFromHistory": "Remove from search history?", + "@documentSearchPageRemoveFromHistory": {}, + "documentSearchResults": "Results", + "@documentSearchResults": {}, "documentsEmptyStateResetFilterLabel": "Zrušit", "@documentsEmptyStateResetFilterLabel": {}, "documentsFilterPageAdvancedLabel": "Rozšířené", @@ -570,6 +580,12 @@ "@settingsPageApplicationSettingsDescriptionText": {}, "settingsPageApplicationSettingsLabel": "Aplikace", "@settingsPageApplicationSettingsLabel": {}, + "settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", + "@settingsPageColorSchemeSettingDialogDescription": {}, + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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.", + "@settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "@settingsPageColorSchemeSettingLabel": {}, "settingsPageLanguageSettingLabel": "Jazyk", "@settingsPageLanguageSettingLabel": {}, "settingsPageSecuritySettingsDescriptionText": "Biometrické ověření", @@ -611,11 +627,5 @@ "verifyIdentityPageTitle": "Ověř svou identitu", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu", - "@verifyIdentityPageVerifyIdentityButtonLabel": {}, - "settingsPageColorSchemeSettingLabel": "Colors", - "colorSchemeOptionClassic": "Classic", - "colorSchemeOptionDznamic": "Dynamic", - "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", - "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation.", - "documentSearchPageRemoveFromHistory": "Remove from search history?" + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index dd50fb4..2f5c5e7 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -40,10 +40,14 @@ "@bottomNavDocumentsPageLabel": {}, "bottomNavInboxPageLabel": "Posteingang", "@bottomNavInboxPageLabel": {}, - "bottomNavLabelsPageLabel": "Kennzeichnungen", + "bottomNavLabelsPageLabel": "Labels", "@bottomNavLabelsPageLabel": {}, "bottomNavScannerPageLabel": "Scanner", "@bottomNavScannerPageLabel": {}, + "colorSchemeOptionClassic": "Klassisch", + "@colorSchemeOptionClassic": {}, + "colorSchemeOptionDynamic": "Dynamisch", + "@colorSchemeOptionDynamic": {}, "correspondentFormFieldSearchHintText": "Beginne zu tippen...", "@correspondentFormFieldSearchHintText": {}, "deleteViewDialogContentText": "Möchtest Du diese Ansicht wirklich löschen?", @@ -144,6 +148,12 @@ "@documentScannerPageUploadButtonTooltip": {}, "documentScannerPageUploadFromThisDeviceButtonLabel": "Lade ein Dokument von diesem Gerät hoch", "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, + "documentSearchHistory": "Verlauf", + "@documentSearchHistory": {}, + "documentSearchPageRemoveFromHistory": "Aus dem Suchverlauf entfernen?", + "@documentSearchPageRemoveFromHistory": {}, + "documentSearchResults": "Ergebnisse", + "@documentSearchResults": {}, "documentsEmptyStateResetFilterLabel": "Filter zurücksetzen", "@documentsEmptyStateResetFilterLabel": {}, "documentsFilterPageAdvancedLabel": "Erweitert", @@ -570,6 +580,12 @@ "@settingsPageApplicationSettingsDescriptionText": {}, "settingsPageApplicationSettingsLabel": "Anwendung", "@settingsPageApplicationSettingsLabel": {}, + "settingsPageColorSchemeSettingDialogDescription": "Wähle zwischen einem klassischen Farbschema, das vom traditionellen Paperless-Grün inspiriert ist, oder einem dynamische Farbschema basierend auf den Systemfarben.", + "@settingsPageColorSchemeSettingDialogDescription": {}, + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamische Farbgebung wird nur von Geräten mit Android 12 und höher unterstützt. Das Auswählen des dynamischen Farbschemas hat für Geräte unter Android 12 womöglich keinen Effekt.", + "@settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": {}, + "settingsPageColorSchemeSettingLabel": "Farben", + "@settingsPageColorSchemeSettingLabel": {}, "settingsPageLanguageSettingLabel": "Sprache", "@settingsPageLanguageSettingLabel": {}, "settingsPageSecuritySettingsDescriptionText": "Biometrische Authentifizierung", @@ -611,11 +627,5 @@ "verifyIdentityPageTitle": "Verifiziere deine Identität", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", - "@verifyIdentityPageVerifyIdentityButtonLabel": {}, - "settingsPageColorSchemeSettingLabel": "Colors", - "colorSchemeOptionClassic": "Classic", - "colorSchemeOptionDznamic": "Dynamic", - "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", - "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation.", - "documentSearchPageRemoveFromHistory": "Remove from search history?" + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 04d7266..54de836 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -44,6 +44,10 @@ "@bottomNavLabelsPageLabel": {}, "bottomNavScannerPageLabel": "Scanner", "@bottomNavScannerPageLabel": {}, + "colorSchemeOptionClassic": "Classic", + "@colorSchemeOptionClassic": {}, + "colorSchemeOptionDynamic": "Dynamic", + "@colorSchemeOptionDynamic": {}, "correspondentFormFieldSearchHintText": "Start typing...", "@correspondentFormFieldSearchHintText": {}, "deleteViewDialogContentText": "Do you really want to delete this view?", @@ -144,6 +148,12 @@ "@documentScannerPageUploadButtonTooltip": {}, "documentScannerPageUploadFromThisDeviceButtonLabel": "Upload a document from this device", "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, + "documentSearchHistory": "History", + "@documentSearchHistory": {}, + "documentSearchPageRemoveFromHistory": "Remove from search history?", + "@documentSearchPageRemoveFromHistory": {}, + "documentSearchResults": "Results", + "@documentSearchResults": {}, "documentsEmptyStateResetFilterLabel": "Reset filter", "@documentsEmptyStateResetFilterLabel": {}, "documentsFilterPageAdvancedLabel": "Advanced", @@ -570,6 +580,12 @@ "@settingsPageApplicationSettingsDescriptionText": {}, "settingsPageApplicationSettingsLabel": "Application", "@settingsPageApplicationSettingsLabel": {}, + "settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", + "@settingsPageColorSchemeSettingDialogDescription": {}, + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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.", + "@settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "@settingsPageColorSchemeSettingLabel": {}, "settingsPageLanguageSettingLabel": "Language", "@settingsPageLanguageSettingLabel": {}, "settingsPageSecuritySettingsDescriptionText": "Biometric authentication", @@ -611,11 +627,5 @@ "verifyIdentityPageTitle": "Verify your identity", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", - "@verifyIdentityPageVerifyIdentityButtonLabel": {}, - "settingsPageColorSchemeSettingLabel": "Colors", - "colorSchemeOptionClassic": "Classic", - "colorSchemeOptionDynamic": "Dynamic", - "settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", - "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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.", - "documentSearchPageRemoveFromHistory": "Remove from search history?" + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 230e2d1..98d0d23 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -44,6 +44,10 @@ "@bottomNavLabelsPageLabel": {}, "bottomNavScannerPageLabel": "Tarayıcı", "@bottomNavScannerPageLabel": {}, + "colorSchemeOptionClassic": "Classic", + "@colorSchemeOptionClassic": {}, + "colorSchemeOptionDynamic": "Dynamic", + "@colorSchemeOptionDynamic": {}, "correspondentFormFieldSearchHintText": "Yazmaya başlayın...", "@correspondentFormFieldSearchHintText": {}, "deleteViewDialogContentText": "Bu görünümü gerçekten silmek istiyor musunuz?", @@ -144,6 +148,12 @@ "@documentScannerPageUploadButtonTooltip": {}, "documentScannerPageUploadFromThisDeviceButtonLabel": "Bu cihazdan bir döküman yükleyin", "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, + "documentSearchHistory": "History", + "@documentSearchHistory": {}, + "documentSearchPageRemoveFromHistory": "Remove from search history?", + "@documentSearchPageRemoveFromHistory": {}, + "documentSearchResults": "Results", + "@documentSearchResults": {}, "documentsEmptyStateResetFilterLabel": "Filtreyi sıfırla", "@documentsEmptyStateResetFilterLabel": {}, "documentsFilterPageAdvancedLabel": "Gelişmiş", @@ -570,6 +580,12 @@ "@settingsPageApplicationSettingsDescriptionText": {}, "settingsPageApplicationSettingsLabel": "Uygulama", "@settingsPageApplicationSettingsLabel": {}, + "settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", + "@settingsPageColorSchemeSettingDialogDescription": {}, + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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.", + "@settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": {}, + "settingsPageColorSchemeSettingLabel": "Colors", + "@settingsPageColorSchemeSettingLabel": {}, "settingsPageLanguageSettingLabel": "Dil", "@settingsPageLanguageSettingLabel": {}, "settingsPageSecuritySettingsDescriptionText": "Biyometrik kimlik doğrulama", @@ -611,11 +627,5 @@ "verifyIdentityPageTitle": "Kimliğinizi doğrulayın", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula", - "@verifyIdentityPageVerifyIdentityButtonLabel": {}, - "settingsPageColorSchemeSettingLabel": "Colors", - "colorSchemeOptionClassic": "Classic", - "colorSchemeOptionDznamic": "Dynamic", - "settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.", - "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation.", - "documentSearchPageRemoveFromHistory": "Remove from search history?" + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file From b697dc7d8db3006376250314e93eb9e3630f81bb Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 28 Jan 2023 23:06:27 +0100 Subject: [PATCH 10/25] WIP - Reimplemented document search --- .../sort_field_localization_mapper.dart | 4 +- lib/core/widgets/app_options_popup_menu.dart | 217 ++++++++++++ .../material/search/m3_search_bar.dart | 77 ++--- .../cubit/document_search_cubit.dart | 56 --- .../document_search_delegate.dart | 206 ----------- .../view/document_search_bar.dart | 49 --- .../documents/bloc/documents_cubit.dart | 4 +- .../documents/bloc/documents_state.dart | 4 +- .../documents/view/pages/documents_page.dart | 324 ++++++++++-------- .../view/widgets/documents_empty_state.dart | 4 +- .../widgets/list/adaptive_documents_view.dart | 4 +- .../view/widgets/sort_documents_button.dart | 87 ++--- lib/features/home/view/home_page.dart | 2 +- lib/features/home/view/widget/app_drawer.dart | 50 +-- lib/features/inbox/bloc/inbox_cubit.dart | 4 +- .../inbox/bloc/state/inbox_state.dart | 4 +- lib/features/inbox/view/pages/inbox_page.dart | 8 +- .../labels/view/pages/labels_page.dart | 1 - .../bloc/linked_documents_cubit.dart | 26 +- .../bloc/state/linked_documents_state.dart | 53 ++- .../view/pages/linked_documents_page.dart | 70 ++-- ..._state.dart => paged_documents_state.dart} | 12 +- ..._mixin.dart => paged_documents_mixin.dart} | 4 +- lib/features/scan/view/scanner_page.dart | 1 - .../search/cubit/document_search_cubit.dart | 68 ++++ .../cubit/document_search_state.dart | 19 +- .../cubit/document_search_state.g.dart | 0 .../search/view/document_search_page.dart | 166 +++++++++ .../bloc/application_settings_state.dart | 12 +- lib/features/settings/view/settings_page.dart | 69 ++++ .../widgets/language_selection_setting.dart | 11 +- .../cubit/similar_documents_cubit.dart | 6 +- .../cubit/similar_documents_state.dart | 2 +- lib/helpers/format_helpers.dart | 2 +- 34 files changed, 949 insertions(+), 677 deletions(-) create mode 100644 lib/core/widgets/app_options_popup_menu.dart delete mode 100644 lib/features/document_search/cubit/document_search_cubit.dart delete mode 100644 lib/features/document_search/document_search_delegate.dart delete mode 100644 lib/features/document_search/view/document_search_bar.dart rename lib/features/paged_document_view/model/{documents_paged_state.dart => paged_documents_state.dart} (88%) rename lib/features/paged_document_view/{documents_paging_mixin.dart => paged_documents_mixin.dart} (97%) create mode 100644 lib/features/search/cubit/document_search_cubit.dart rename lib/features/{document_search => search}/cubit/document_search_state.dart (79%) rename lib/features/{document_search => search}/cubit/document_search_state.g.dart (100%) create mode 100644 lib/features/search/view/document_search_page.dart diff --git a/lib/core/translation/sort_field_localization_mapper.dart b/lib/core/translation/sort_field_localization_mapper.dart index c2b1f0b..e707b6e 100644 --- a/lib/core/translation/sort_field_localization_mapper.dart +++ b/lib/core/translation/sort_field_localization_mapper.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -String translateSortField(BuildContext context, SortField sortField) { +String translateSortField(BuildContext context, SortField? sortField) { switch (sortField) { case SortField.archiveSerialNumber: return S.of(context).documentArchiveSerialNumberPropertyShortLabel; @@ -18,5 +18,7 @@ String translateSortField(BuildContext context, SortField sortField) { return S.of(context).documentAddedPropertyLabel; case SortField.modified: return S.of(context).documentModifiedPropertyLabel; + default: + return ''; } } diff --git a/lib/core/widgets/app_options_popup_menu.dart b/lib/core/widgets/app_options_popup_menu.dart new file mode 100644 index 0000000..ba97902 --- /dev/null +++ b/lib/core/widgets/app_options_popup_menu.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/constants.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; +import 'package:paperless_mobile/features/settings/view/settings_page.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:url_launcher/link.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +/// Declares selectable actions in menu. +enum AppPopupMenuEntries { + // Documents preview + documentsSelectListView, + documentsSelectGridView, + // Generic actions + openAboutThisAppDialog, + reportBug, + openSettings, + // Adds a divider + divider; +} + +class AppOptionsPopupMenu extends StatelessWidget { + final List displayedActions; + const AppOptionsPopupMenu({ + super.key, + required this.displayedActions, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + position: PopupMenuPosition.under, + icon: const Icon(Icons.more_vert), + onSelected: (action) { + switch (action) { + case AppPopupMenuEntries.documentsSelectListView: + context.read().setViewType(ViewType.list); + break; + case AppPopupMenuEntries.documentsSelectGridView: + context.read().setViewType(ViewType.grid); + break; + case AppPopupMenuEntries.openAboutThisAppDialog: + _showAboutDialog(context); + break; + case AppPopupMenuEntries.openSettings: + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: context.read(), + child: const SettingsPage(), + ), + ), + ); + break; + case AppPopupMenuEntries.reportBug: + launchUrlString( + 'https://github.com/astubenbord/paperless-mobile/issues/new', + ); + break; + default: + break; + } + }, + itemBuilder: _buildEntries, + ); + } + + PopupMenuItem _buildReportBugTile(BuildContext context) { + return PopupMenuItem( + value: AppPopupMenuEntries.reportBug, + padding: EdgeInsets.zero, + child: ListTile( + leading: const Icon(Icons.bug_report), + title: Text(S.of(context).appDrawerReportBugLabel), + ), + ); + } + + PopupMenuItem _buildSettingsTile(BuildContext context) { + return PopupMenuItem( + padding: EdgeInsets.zero, + value: AppPopupMenuEntries.openSettings, + child: ListTile( + leading: const Icon(Icons.settings_outlined), + title: Text(S.of(context).appDrawerSettingsLabel), + ), + ); + } + + PopupMenuItem _buildAboutTile(BuildContext context) { + return PopupMenuItem( + padding: EdgeInsets.zero, + value: AppPopupMenuEntries.openAboutThisAppDialog, + child: ListTile( + leading: const Icon(Icons.info_outline), + title: Text(S.of(context).appDrawerAboutLabel), + ), + ); + } + + PopupMenuItem _buildListViewTile() { + return PopupMenuItem( + padding: EdgeInsets.zero, + child: BlocBuilder( + builder: (context, state) { + return ListTile( + leading: const Icon(Icons.list), + title: const Text("List"), + trailing: state.preferredViewType == ViewType.list + ? const Icon(Icons.check) + : null, + ); + }, + ), + value: AppPopupMenuEntries.documentsSelectListView, + ); + } + + PopupMenuItem _buildGridViewTile() { + return PopupMenuItem( + value: AppPopupMenuEntries.documentsSelectGridView, + padding: EdgeInsets.zero, + child: BlocBuilder( + builder: (context, state) { + return ListTile( + leading: const Icon(Icons.grid_view_rounded), + title: const Text("Grid"), + trailing: state.preferredViewType == ViewType.grid + ? const Icon(Icons.check) + : null, + ); + }, + ), + ); + } + + void _showAboutDialog(BuildContext context) { + showAboutDialog( + context: context, + applicationIcon: const ImageIcon( + AssetImage('assets/logos/paperless_logo_green.png'), + ), + applicationName: 'Paperless Mobile', + applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, + children: [ + Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), + Link( + uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + 'https://github.com/astubenbord/paperless-mobile', + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Credits', + style: Theme.of(context).textTheme.titleMedium, + ), + _buildOnboardingImageCredits(), + ], + ); + } + + Widget _buildOnboardingImageCredits() { + return Link( + uri: Uri.parse( + 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), + builder: (context, followLink) => Wrap( + children: [ + const Text('Onboarding images by '), + GestureDetector( + onTap: followLink, + child: Text( + 'pch.vector', + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), + ), + ), + const Text(' on Freepik.') + ], + ), + ); + } + + List> _buildEntries( + BuildContext context) { + List> items = []; + for (final entry in displayedActions) { + switch (entry) { + case AppPopupMenuEntries.documentsSelectListView: + items.add(_buildListViewTile()); + break; + case AppPopupMenuEntries.documentsSelectGridView: + items.add(_buildGridViewTile()); + break; + case AppPopupMenuEntries.openAboutThisAppDialog: + items.add(_buildAboutTile(context)); + break; + case AppPopupMenuEntries.reportBug: + items.add(_buildReportBugTile(context)); + break; + case AppPopupMenuEntries.openSettings: + items.add(_buildSettingsTile(context)); + break; + case AppPopupMenuEntries.divider: + items.add(const PopupMenuDivider()); + break; + } + } + return items; + } +} diff --git a/lib/core/widgets/material/search/m3_search_bar.dart b/lib/core/widgets/material/search/m3_search_bar.dart index 1ec56ce..eb75144 100644 --- a/lib/core/widgets/material/search/m3_search_bar.dart +++ b/lib/core/widgets/material/search/m3_search_bar.dart @@ -28,51 +28,48 @@ class SearchBar extends StatelessWidget { final ColorScheme colorScheme = Theme.of(context).colorScheme; final TextTheme textTheme = Theme.of(context).textTheme; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Container( - constraints: const BoxConstraints(minWidth: 360, maxWidth: 720), - width: double.infinity, - height: effectiveHeight, - child: Material( - elevation: 3, - color: colorScheme.surface, - shadowColor: colorScheme.shadow, - surfaceTintColor: colorScheme.surfaceTint, + return Container( + constraints: const BoxConstraints(minWidth: 360, maxWidth: 720), + width: double.infinity, + height: effectiveHeight, + child: Material( + elevation: 1, + color: colorScheme.surface, + shadowColor: colorScheme.shadow, + surfaceTintColor: colorScheme.surfaceTint, + borderRadius: BorderRadius.circular(effectiveHeight / 2), + child: InkWell( + onTap: () {}, borderRadius: BorderRadius.circular(effectiveHeight / 2), - child: InkWell( - onTap: () {}, - borderRadius: BorderRadius.circular(effectiveHeight / 2), - highlightColor: Colors.transparent, - splashFactory: InkRipple.splashFactory, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row(children: [ - leadingIcon, - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8), - child: TextField( - cursorColor: colorScheme.primary, - style: textTheme.bodyLarge, - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - isCollapsed: true, - border: InputBorder.none, - contentPadding: - const EdgeInsets.symmetric(horizontal: 8), - hintText: supportingText, - hintStyle: textTheme.bodyLarge?.apply( - color: colorScheme.onSurfaceVariant, - ), + highlightColor: Colors.transparent, + splashFactory: InkRipple.splashFactory, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row(children: [ + leadingIcon, + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: TextField( + readOnly: true, + cursorColor: colorScheme.primary, + style: textTheme.bodyLarge, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + hintText: supportingText, + hintStyle: textTheme.bodyLarge?.apply( + color: colorScheme.onSurfaceVariant, ), - onTap: onTap, ), + onTap: onTap, ), ), - if (trailingIcon != null) trailingIcon!, - ]), - ), + ), + if (trailingIcon != null) trailingIcon!, + ]), ), ), ), diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart deleted file mode 100644 index 26d39bc..0000000 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; - -import 'document_search_state.dart'; - -class DocumentSearchCubit extends HydratedCubit - with DocumentsPagingMixin { - //// - DocumentSearchCubit(this.api) : super(const DocumentSearchState()); - - @override - final PaperlessDocumentsApi api; - - /// - /// Requests results based on [query] and adds [query] to the - /// search history, removing old occurrences and trimming the list to - /// the last 5 searches. - /// - Future updateResults(String query) async { - await updateFilter( - filter: state.filter.copyWith(query: TextQuery.titleAndContent(query)), - ); - emit( - state.copyWith( - searchHistory: [ - query, - ...state.searchHistory.where((element) => element != query) - ].take(5).toList(), - ), - ); - } - - void removeHistoryEntry(String suggestion) { - emit(state.copyWith( - searchHistory: state.searchHistory - .whereNot((element) => element == suggestion) - .toList(), - )); - } - - Future> findSuggestions(String query) { - return api.autocomplete(query); - } - - @override - DocumentSearchState? fromJson(Map json) { - return DocumentSearchState.fromJson(json); - } - - @override - Map? toJson(DocumentSearchState state) { - return state.toJson(); - } -} diff --git a/lib/features/document_search/document_search_delegate.dart b/lib/features/document_search/document_search_delegate.dart deleted file mode 100644 index 17e8e3b..0000000 --- a/lib/features/document_search/document_search_delegate.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; -import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; -import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; -import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; - -import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart' - as m3; -import 'package:paperless_mobile/generated/l10n.dart'; - -class DocumentSearchDelegate extends m3.SearchDelegate { - final DocumentSearchCubit bloc; - DocumentSearchDelegate( - this.bloc, { - required String hintText, - required super.searchFieldStyle, - }) : super( - searchFieldLabel: hintText, - keyboardType: TextInputType.text, - textInputAction: TextInputAction.search, - ); - - @override - Widget buildLeading(BuildContext context) => const BackButton(); - - @override - PreferredSizeWidget buildBottom(BuildContext context) => PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Divider( - color: Theme.of(context).colorScheme.outline, - height: 1, - ), - ); - @override - Widget buildSuggestions(BuildContext context) { - return BlocBuilder( - bloc: bloc, - builder: (context, state) { - if (query.isEmpty) { - return CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Text( - S.of(context).documentSearchHistory, - style: Theme.of(context).textTheme.labelMedium, - ).padded(16), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final label = state.searchHistory[index]; - return ListTile( - leading: const Icon(Icons.history), - title: Text(label), - onTap: () => _onSuggestionSelected( - context, - label, - ), - onLongPress: () => showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(label), - content: Text( - S.of(context).documentSearchPageRemoveFromHistory, - ), - actions: [ - TextButton( - child: Text( - S.of(context).genericActionCancelLabel, - ), - onPressed: () => Navigator.pop(context), - ), - TextButton( - child: Text( - S.of(context).genericActionDeleteLabel, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ), - onPressed: () { - bloc.removeHistoryEntry(label); - Navigator.pop(context); - }, - ), - ], - ), - ), - ); - }, - childCount: state.searchHistory.length, - ), - ), - ], - ); - } - return FutureBuilder>( - future: bloc.findSuggestions(query), - builder: (context, snapshot) { - final historyMatches = state.searchHistory - .where((e) => e.startsWith(query)) - .toList(); - final serverSuggestions = (snapshot.data ?? []) - ..removeWhere((e) => historyMatches.contains(e)); - return CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Text( - S.of(context).documentSearchResults, - style: Theme.of(context).textTheme.labelMedium, - ).padded(), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => ListTile( - title: Text(historyMatches[index]), - leading: const Icon(Icons.history), - onTap: () => _onSuggestionSelected( - context, - historyMatches[index], - ), - ), - childCount: historyMatches.length, - ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => ListTile( - title: Text(serverSuggestions[index]), - leading: const Icon(Icons.search), - onTap: () => _onSuggestionSelected( - context, snapshot.data![index]), - ), - childCount: serverSuggestions.length, - ), - ), - ], - ); - }); - }, - ); - } - - void _onSuggestionSelected(BuildContext context, String suggestion) { - query = suggestion; - bloc.updateResults(query); - super.showResults(context); - } - - @override - Widget buildResults(BuildContext context) { - return BlocBuilder( - bloc: bloc, - builder: (context, state) { - if (!state.hasLoaded && state.isLoading) { - return const DocumentsListLoadingWidget(); - } - return ListView.builder( - itemCount: state.documents.length, - itemBuilder: (context, index) => DocumentListItem( - document: state.documents[index], - onTap: (document) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider( - create: (context) => DocumentDetailsCubit( - context.read(), - document, - ), - child: const LabelRepositoriesProvider( - child: DocumentDetailsPage( - isLabelClickable: false, - ), - ), - ), - ), - ); - }, - ), - ); - }, - ); - } - - @override - List? buildActions(BuildContext context) { - return [ - IconButton( - icon: Icon( - Icons.clear, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ).paddedSymmetrically(horizontal: 16), - onPressed: () { - query = ''; - super.showSuggestions(context); - }, - ), - ]; - } -} diff --git a/lib/features/document_search/view/document_search_bar.dart b/lib/features/document_search/view/document_search_bar.dart deleted file mode 100644 index 2f70365..0000000 --- a/lib/features/document_search/view/document_search_bar.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart'; -import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; -import 'package:paperless_mobile/features/document_search/document_search_delegate.dart'; -import 'package:provider/provider.dart'; - -class DocumentSearchBar extends StatelessWidget { - const DocumentSearchBar({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return TextField( - onTap: () => showMaterial3Search( - context: context, - delegate: DocumentSearchDelegate( - DocumentSearchCubit(context.read()), - searchFieldStyle: Theme.of(context).textTheme.bodyLarge, - hintText: "Search documents", - ), - ), - readOnly: true, - decoration: InputDecoration( - hintText: "Search documents", - hintStyle: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), - filled: true, - fillColor: Theme.of(context).colorScheme.surfaceVariant, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(56), - borderSide: BorderSide.none, - ), - prefixIcon: IconButton( - icon: const Icon(Icons.search), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ), - constraints: const BoxConstraints(maxHeight: 48), - ), - // title: Text( - // "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})", - // ), - ); - } -} diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 1eb3e32..428fb91 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -5,10 +5,10 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; +import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; class DocumentsCubit extends HydratedCubit - with DocumentsPagingMixin { + with PagedDocumentsMixin { @override final PaperlessDocumentsApi api; diff --git a/lib/features/documents/bloc/documents_state.dart b/lib/features/documents/bloc/documents_state.dart index 2850d7c..23adc83 100644 --- a/lib/features/documents/bloc/documents_state.dart +++ b/lib/features/documents/bloc/documents_state.dart @@ -1,8 +1,8 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; -class DocumentsState extends DocumentsPagedState { +class DocumentsState extends PagedDocumentsState { final int? selectedSavedViewId; @JsonKey(ignore: true) diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index a2a9f96..0777db5 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -5,8 +5,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart'; +import 'package:paperless_mobile/core/widgets/app_options_popup_menu.dart'; import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; @@ -25,6 +27,7 @@ import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_prov import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart'; +import 'package:paperless_mobile/features/search/view/document_search_page.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; @@ -32,7 +35,6 @@ import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/constants.dart'; class DocumentFilterIntent { final DocumentFilter? filter; @@ -137,142 +139,151 @@ class _DocumentsPageState extends State { } }, builder: (context, connectivityState) { - const linearProgressIndicatorHeight = 4.0; return Scaffold( - drawer: BlocProvider.value( - value: context.read(), - child: AppDrawer( - afterInboxClosed: () => context.read().reload(), - ), - ), - appBar: PreferredSize( - preferredSize: const Size.fromHeight( - kToolbarHeight, - ), - child: BlocBuilder( - builder: (context, state) { - if (state.selection.isEmpty) { - return AppBar( - automaticallyImplyLeading: true, - title: Text(S.of(context).documentsPageTitle + - " (${formatMaxCount(state.documents.length)})"), - actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () { - showMaterial3Search( - context: context, - delegate: DocumentSearchDelegate( - DocumentSearchCubit(context.read()), - searchFieldStyle: - Theme.of(context).textTheme.bodyLarge, - hintText: "Search documents", - ), - ); - }, - ), - const SortDocumentsButton(), - BlocBuilder( - builder: (context, settingsState) => IconButton( - icon: Icon( - settingsState.preferredViewType == ViewType.grid - ? Icons.list - : Icons.grid_view_rounded, - ), - onPressed: () { - // Reset saved view widget position as scroll offset will be reset anyway. - setState(() { - _offset = 0; - _last = 0; - }); - final cubit = - context.read(); - cubit.setViewType( - cubit.state.preferredViewType.toggle()); - }, - ), - ), - ], - ); - } else { - return AppBar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => - context.read().resetSelection(), - ), - title: Text( - '${state.selection.length} ${S.of(context).documentsSelectedText}'), - actions: [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _onDelete(context, state), - ), - ], - ); - } - }, - ), - ), - floatingActionButton: BlocBuilder( - builder: (context, state) { - final appliedFiltersCount = state.filter.appliedFiltersCount; - return b.Badge( - position: b.BadgePosition.topEnd(top: -12, end: -6), - showBadge: appliedFiltersCount > 0, - badgeContent: Text( - '$appliedFiltersCount', - style: const TextStyle( - color: Colors.white, - ), - ), - animationType: b.BadgeAnimationType.fade, - badgeColor: Colors.red, - child: FloatingActionButton( - child: const Icon(Icons.filter_alt_outlined), - onPressed: _openDocumentFilter, - ), - ); - }, - ), + // appBar: PreferredSize( + // preferredSize: const Size.fromHeight( + // kToolbarHeight, + // ), + // child: BlocBuilder( + // builder: (context, state) { + // if (state.selection.isEmpty) { + // return DocumentSearchBar(); + // // return AppBar( + // // title: Text(S.of(context).documentsPageTitle + + // // " (${formatMaxCount(state.documents.length)})"), + // // actions: [ + // // IconButton( + // // icon: const Icon(Icons.search), + // // onPressed: () { + // // showMaterial3Search( + // // context: context, + // // delegate: DocumentSearchDelegate( + // // DocumentSearchCubit(context.read()), + // // searchFieldStyle: + // // Theme.of(context).textTheme.bodyLarge, + // // hintText: "Search documents", //TODO: INTL + // // ), + // // ); + // // }, + // // ), + // // const SortDocumentsButton(), + // // const AppOptionsPopupMenu( + // // displayedActions: [ + // // AppPopupMenuEntries.documentsSelectListView, + // // AppPopupMenuEntries.documentsSelectGridView, + // // AppPopupMenuEntries.divider, + // // AppPopupMenuEntries.openAboutThisAppDialog, + // // AppPopupMenuEntries.reportBug, + // // AppPopupMenuEntries.openSettings, + // // ], + // // ), + // // ], + // // ); + // } else { + // return AppBar( + // leading: IconButton( + // icon: const Icon(Icons.close), + // onPressed: () => + // context.read().resetSelection(), + // ), + // title: Text( + // '${state.selection.length} ${S.of(context).documentsSelectedText}'), + // actions: [ + // IconButton( + // icon: const Icon(Icons.delete), + // onPressed: () => _onDelete(context, state), + // ), + // ], + // ); + // } + // }, + // ), + // ), + // floatingActionButton: BlocBuilder( + // builder: (context, state) { + // final appliedFiltersCount = state.filter.appliedFiltersCount; + // return b.Badge( + // position: b.BadgePosition.topEnd(top: -12, end: -6), + // showBadge: appliedFiltersCount > 0, + // badgeContent: Text( + // '$appliedFiltersCount', + // style: const TextStyle( + // color: Colors.white, + // ), + // ), + // animationType: b.BadgeAnimationType.fade, + // badgeColor: Colors.red, + // child: FloatingActionButton( + // child: const Icon(Icons.filter_alt_outlined), + // onPressed: _openDocumentFilter, + // ), + // ); + // }, + // ), resizeToAvoidBottomInset: true, - body: WillPopScope( - onWillPop: () async { - if (context.read().state.selection.isNotEmpty) { - context.read().resetSelection(); - } - return false; + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverAppBar( + floating: true, + pinned: true, + snap: true, + title: SearchBar( + height: kToolbarHeight - 2, + supportingText: "Search documents", + onTap: () { + showDocumentSearchPage(context); + }, + leadingIcon: Icon(Icons.menu), + trailingIcon: CircleAvatar( + child: Text("A"), + ), + ), + ) + ]; }, - child: RefreshIndicator( - onRefresh: _onRefresh, - notificationPredicate: (_) => connectivityState.isConnected, - child: BlocBuilder( - builder: (context, taskState) { - return Stack( - children: [ - _buildBody(connectivityState), - Positioned( - left: 0, - right: 0, - top: _offset, - child: BlocBuilder( - builder: (context, state) { - return ColoredBox( - color: Theme.of(context).colorScheme.background, - child: SavedViewSelectionWidget( - height: _savedViewWidgetHeight, - currentFilter: state.filter, - enabled: state.selection.isEmpty && - connectivityState.isConnected, - ), - ); - }, - ), - ), - ], - ); - }, + body: WillPopScope( + onWillPop: () async { + if (context + .read() + .state + .selection + .isNotEmpty) { + context.read().resetSelection(); + } + return false; + }, + child: RefreshIndicator( + onRefresh: _onRefresh, + notificationPredicate: (_) => connectivityState.isConnected, + child: BlocBuilder( + builder: (context, taskState) { + return _buildBody(connectivityState); + // return Stack( + // children: [ + // Positioned( + // left: 0, + // right: 0, + // top: _offset, + // child: BlocBuilder( + // builder: (context, state) { + // return ColoredBox( + // color: + // Theme.of(context).colorScheme.background, + // child: SavedViewSelectionWidget( + // height: _savedViewWidgetHeight, + // currentFilter: state.filter, + // enabled: state.selection.isEmpty && + // connectivityState.isConnected, + // ), + // ); + // }, + // ), + // ), + // ], + // ); + }, + ), ), ), ), @@ -282,6 +293,28 @@ class _DocumentsPageState extends State { ); } + BlocBuilder + _buildViewTypeButton() { + return BlocBuilder( + builder: (context, settingsState) => IconButton( + icon: Icon( + settingsState.preferredViewType == ViewType.grid + ? Icons.list + : Icons.grid_view_rounded, + ), + onPressed: () { + // Reset saved view widget position as scroll offset will be reset anyway. + setState(() { + _offset = 0; + _last = 0; + }); + final cubit = context.read(); + cubit.setViewType(cubit.state.preferredViewType.toggle()); + }, + ), + ); + } + void _onDelete(BuildContext context, DocumentsState documentsState) async { final shouldDelete = await showDialog( context: context, @@ -392,7 +425,26 @@ class _DocumentsPageState extends State { onDocumentTypeSelected: _addDocumentTypeToFilter, onStoragePathSelected: _addStoragePathToFilter, pageLoadingWidget: const NewItemsLoadingWidget(), - beforeItems: const SizedBox(height: _savedViewWidgetHeight), + beforeItems: SizedBox( + height: kToolbarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SortDocumentsButton(), + IconButton( + icon: Icon( + settings.preferredViewType == ViewType.grid + ? Icons.list + : Icons.grid_view_rounded, + ), + onPressed: () => + context.read().setViewType( + settings.preferredViewType.toggle(), + ), + ), + ], + ), + ), ); }, ); diff --git a/lib/features/documents/view/widgets/documents_empty_state.dart b/lib/features/documents/view/widgets/documents_empty_state.dart index 0d5e6cd..9612fc7 100644 --- a/lib/features/documents/view/widgets/documents_empty_state.dart +++ b/lib/features/documents/view/widgets/documents_empty_state.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/empty_state.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class DocumentsEmptyState extends StatelessWidget { - final DocumentsPagedState state; + final PagedDocumentsState state; final VoidCallback onReset; const DocumentsEmptyState({ Key? key, diff --git a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart index 1f2b8d8..4e750db 100644 --- a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart @@ -9,7 +9,7 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart'; class AdaptiveDocumentsView extends StatelessWidget { final DocumentsState state; final ViewType viewType; - final Widget beforeItems; + final Widget? beforeItems; final void Function(DocumentModel) onTap; final void Function(DocumentModel) onSelected; final ScrollController scrollController; @@ -34,7 +34,7 @@ class AdaptiveDocumentsView extends StatelessWidget { this.onDocumentTypeSelected, this.onStoragePathSelected, required this.pageLoadingWidget, - required this.beforeItems, + this.beforeItems, required this.viewType, }); diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index cccc276..a4610a8 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -4,51 +4,60 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; +import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; class SortDocumentsButton extends StatelessWidget { - const SortDocumentsButton({super.key}); + const SortDocumentsButton({ + super.key, + }); @override Widget build(BuildContext context) { - return IconButton( - icon: const Icon(Icons.sort), - onPressed: () { - showModalBottomSheet( - elevation: 2, - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - builder: (_) => BlocProvider.value( - value: context.read(), - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), - ), + return BlocBuilder( + builder: (context, state) { + if (state.filter.sortField == null) { + return const SizedBox.shrink(); + } + return TextButton.icon( + icon: Icon(state.filter.sortOrder == SortOrder.ascending + ? Icons.arrow_upward + : Icons.arrow_downward), + label: Text(translateSortField(context, state.filter.sortField)), + onPressed: () { + showModalBottomSheet( + elevation: 2, + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), ), - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), - ), - ), - ], - child: BlocBuilder( - builder: (context, state) { - return SortFieldSelectionBottomSheet( + ), + builder: (_) => BlocProvider.value( + value: context.read(), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), + ), + ), + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), + ), + ), + ], + child: SortFieldSelectionBottomSheet( initialSortField: state.filter.sortField, initialSortOrder: state.filter.sortOrder, onSubmit: (field, order) => @@ -58,11 +67,11 @@ class SortDocumentsButton extends StatelessWidget { sortOrder: order, ), ), - ); - }, + ), + ), ), - ), - ), + ); + }, ); }, ); diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index d5baa79..b033504 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -249,7 +249,7 @@ class _HomePageState extends State { builder: (context, sizingInformation) { if (!sizingInformation.isMobile) { return Scaffold( - drawer: const AppDrawer(), + // drawer: const AppDrawer(), body: Row( children: [ NavigationRail( diff --git a/lib/features/home/view/widget/app_drawer.dart b/lib/features/home/view/widget/app_drawer.dart index f6b5c98..344a928 100644 --- a/lib/features/home/view/widget/app_drawer.dart +++ b/lib/features/home/view/widget/app_drawer.dart @@ -317,53 +317,5 @@ class _AppDrawerState extends State { ); } - Link _buildOnboardingImageCredits() { - return Link( - uri: Uri.parse( - 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), - builder: (context, followLink) => Wrap( - children: [ - const Text('Onboarding images by '), - GestureDetector( - onTap: followLink, - child: Text( - 'pch.vector', - style: TextStyle(color: Theme.of(context).colorScheme.tertiary), - ), - ), - const Text(' on Freepik.') - ], - ), - ); - } - - void _onShowAboutDialog() { - showAboutDialog( - context: context, - applicationIcon: const ImageIcon( - AssetImage('assets/logos/paperless_logo_green.png'), - ), - applicationName: 'Paperless Mobile', - applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, - children: [ - Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), - Link( - uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), - builder: (context, followLink) => GestureDetector( - onTap: followLink, - child: Text( - 'https://github.com/astubenbord/paperless-mobile', - style: TextStyle(color: Theme.of(context).colorScheme.tertiary), - ), - ), - ), - const SizedBox(height: 16), - Text( - 'Credits', - style: Theme.of(context).textTheme.titleMedium, - ), - _buildOnboardingImageCredits(), - ], - ); - } + void _onShowAboutDialog() {} } diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index 22da765..2d2651a 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -7,9 +7,9 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; -import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; +import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; -class InboxCubit extends HydratedCubit with DocumentsPagingMixin { +class InboxCubit extends HydratedCubit with PagedDocumentsMixin { final LabelRepository _tagsRepository; final LabelRepository _correspondentRepository; diff --git a/lib/features/inbox/bloc/state/inbox_state.dart b/lib/features/inbox/bloc/state/inbox_state.dart index 186c501..be2aafc 100644 --- a/lib/features/inbox/bloc/state/inbox_state.dart +++ b/lib/features/inbox/bloc/state/inbox_state.dart @@ -1,13 +1,13 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; part 'inbox_state.g.dart'; @JsonSerializable( ignoreUnannotated: true, ) -class InboxState extends DocumentsPagedState { +class InboxState extends PagedDocumentsState { final Iterable inboxTags; final Map availableTags; diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 1f0245d..9028c2d 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -72,10 +72,10 @@ class _InboxPageState extends State { child: ColoredBox( color: Theme.of(context).colorScheme.secondaryContainer, child: Text( - state.value.isEmpty - ? '0' - : '${state.value.first.count} ' + - S.of(context).inboxPageUnseenText, + (state.value.isEmpty + ? '0 ' + : '${state.value.first.count} ') + + S.of(context).inboxPageUnseenText, textAlign: TextAlign.start, style: Theme.of(context).textTheme.bodySmall, ).paddedSymmetrically(horizontal: 4.0), diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 0876dc7..f84bb3a 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -51,7 +51,6 @@ class _LabelsPageState extends State child: BlocBuilder( builder: (context, connectedState) { return Scaffold( - drawer: const AppDrawer(), appBar: AppBar( title: Text( [ diff --git a/lib/features/linked_documents/bloc/linked_documents_cubit.dart b/lib/features/linked_documents/bloc/linked_documents_cubit.dart index cf77aa6..ffc9343 100644 --- a/lib/features/linked_documents/bloc/linked_documents_cubit.dart +++ b/lib/features/linked_documents/bloc/linked_documents_cubit.dart @@ -1,25 +1,15 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; -class LinkedDocumentsCubit extends Cubit { - final PaperlessDocumentsApi _api; +class LinkedDocumentsCubit extends Cubit + with PagedDocumentsMixin { + @override + final PaperlessDocumentsApi api; - LinkedDocumentsCubit(this._api, DocumentFilter filter) - : super(LinkedDocumentsState(filter: filter)) { - _initialize(); - } - - Future _initialize() async { - final documents = await _api.findAll( - state.filter.copyWith( - pageSize: 100, - ), - ); - emit(LinkedDocumentsState( - isLoaded: true, - documents: documents, - filter: state.filter, - )); + LinkedDocumentsCubit(this.api, DocumentFilter filter) + : super(const LinkedDocumentsState()) { + updateFilter(filter: filter); } } diff --git a/lib/features/linked_documents/bloc/state/linked_documents_state.dart b/lib/features/linked_documents/bloc/state/linked_documents_state.dart index abb2f4b..d72a3e5 100644 --- a/lib/features/linked_documents/bloc/state/linked_documents_state.dart +++ b/lib/features/linked_documents/bloc/state/linked_documents_state.dart @@ -1,13 +1,48 @@ import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; -class LinkedDocumentsState { - final bool isLoaded; - final PagedSearchResult? documents; - final DocumentFilter filter; - - LinkedDocumentsState({ - required this.filter, - this.isLoaded = false, - this.documents, +class LinkedDocumentsState extends PagedDocumentsState { + const LinkedDocumentsState({ + super.filter, + super.isLoading, + super.hasLoaded, + super.value, }); + + LinkedDocumentsState copyWith({ + DocumentFilter? filter, + bool? isLoading, + bool? hasLoaded, + List>? value, + }) { + return LinkedDocumentsState( + filter: filter ?? this.filter, + isLoading: isLoading ?? this.isLoading, + hasLoaded: hasLoaded ?? this.hasLoaded, + value: value ?? this.value, + ); + } + + @override + LinkedDocumentsState copyWithPaged({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return copyWith( + hasLoaded: hasLoaded, + isLoading: isLoading, + value: value, + filter: filter, + ); + } + + @override + List get props => [ + filter, + isLoading, + hasLoaded, + value, + ]; } diff --git a/lib/features/linked_documents/view/pages/linked_documents_page.dart b/lib/features/linked_documents/view/pages/linked_documents_page.dart index 5cce90f..b741104 100644 --- a/lib/features/linked_documents/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents/view/pages/linked_documents_page.dart @@ -8,6 +8,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/list/document_l import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; class LinkedDocumentsPage extends StatefulWidget { const LinkedDocumentsPage({super.key}); @@ -17,6 +18,28 @@ class LinkedDocumentsPage extends StatefulWidget { } class _LinkedDocumentsPageState extends State { + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_listenForLoadNewData); + } + + void _listenForLoadNewData() async { + final currState = context.read().state; + if (_scrollController.offset >= + _scrollController.position.maxScrollExtent * 0.75 && + !currState.isLoading && + !currState.isLastPageLoaded) { + try { + await context.read().loadMore(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -25,45 +48,14 @@ class _LinkedDocumentsPageState extends State { ), body: BlocBuilder( builder: (context, state) { - return Column( - children: [ - Text( - S.of(context).referencedDocumentsReadOnlyHintText, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall, - ), - if (!state.isLoaded) - const Expanded(child: DocumentsListLoadingWidget()) - else - Expanded( - child: ListView.builder( - itemCount: state.documents?.results.length, - itemBuilder: (context, index) { - return DocumentListItem( - isLabelClickable: false, - document: state.documents!.results.elementAt(index), - onTap: (doc) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider( - create: (context) => DocumentDetailsCubit( - context.read(), - state.documents!.results.elementAt(index), - ), - child: const DocumentDetailsPage( - isLabelClickable: false, - allowEdit: false, - ), - ), - ), - ); - }, - ); - }, - ), - ), - ], + if (!state.hasLoaded) { + return const DocumentsListLoadingWidget(); + } + return ListView.builder( + itemCount: state.documents.length, + itemBuilder: (context, index) => DocumentListItem( + document: state.documents[index], + ), ); }, ), diff --git a/lib/features/paged_document_view/model/documents_paged_state.dart b/lib/features/paged_document_view/model/paged_documents_state.dart similarity index 88% rename from lib/features/paged_document_view/model/documents_paged_state.dart rename to lib/features/paged_document_view/model/paged_documents_state.dart index 71df68b..e50fe46 100644 --- a/lib/features/paged_document_view/model/documents_paged_state.dart +++ b/lib/features/paged_document_view/model/paged_documents_state.dart @@ -5,13 +5,13 @@ import 'package:paperless_api/paperless_api.dart'; /// Base state for all blocs/cubits using a paged view of documents. /// [T] is the return type of the API call. /// -abstract class DocumentsPagedState extends Equatable { +abstract class PagedDocumentsState extends Equatable { final bool hasLoaded; final bool isLoading; final List> value; final DocumentFilter filter; - const DocumentsPagedState({ + const PagedDocumentsState({ this.value = const [], this.hasLoaded = false, this.isLoading = false, @@ -71,4 +71,12 @@ abstract class DocumentsPagedState extends Equatable { List>? value, DocumentFilter? filter, }); + + @override + List get props => [ + filter, + value, + hasLoaded, + isLoading, + ]; } diff --git a/lib/features/paged_document_view/documents_paging_mixin.dart b/lib/features/paged_document_view/paged_documents_mixin.dart similarity index 97% rename from lib/features/paged_document_view/documents_paging_mixin.dart rename to lib/features/paged_document_view/paged_documents_mixin.dart index d012c9b..687d410 100644 --- a/lib/features/paged_document_view/documents_paging_mixin.dart +++ b/lib/features/paged_document_view/paged_documents_mixin.dart @@ -2,12 +2,12 @@ import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'model/documents_paged_state.dart'; +import 'model/paged_documents_state.dart'; /// /// Mixin which can be used on cubits which handle documents. This implements all paging and filtering logic. /// -mixin DocumentsPagingMixin +mixin PagedDocumentsMixin on BlocBase { PaperlessDocumentsApi get api; diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 37f2776..bfa8fc4 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -47,7 +47,6 @@ class _ScannerPageState extends State return BlocBuilder( builder: (context, connectedState) { return Scaffold( - drawer: const AppDrawer(), floatingActionButton: FloatingActionButton( onPressed: () => _openDocumentScanner(context), child: const Icon(Icons.add_a_photo_outlined), diff --git a/lib/features/search/cubit/document_search_cubit.dart b/lib/features/search/cubit/document_search_cubit.dart new file mode 100644 index 0000000..390c68b --- /dev/null +++ b/lib/features/search/cubit/document_search_cubit.dart @@ -0,0 +1,68 @@ +import 'package:collection/collection.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; +import 'package:paperless_mobile/features/search/cubit/document_search_state.dart'; + +class DocumentSearchCubit extends HydratedCubit + with PagedDocumentsMixin { + @override + final PaperlessDocumentsApi api; + DocumentSearchCubit(this.api) : super(const DocumentSearchState()); + + Future search(String query) async { + emit(state.copyWith( + isLoading: true, + suggestions: [], + view: SearchView.results, + )); + final searchFilter = DocumentFilter( + query: TextQuery.titleAndContent(query), + ); + + await updateFilter(filter: searchFilter); + emit( + state.copyWith( + searchHistory: [ + query, + ...state.searchHistory + .whereNot((previousQuery) => previousQuery == query) + ], + ), + ); + } + + Future suggest(String query) async { + emit( + state.copyWith( + isLoading: true, + view: SearchView.suggestions, + value: [], + suggestions: [], + ), + ); + final suggestions = await api.autocomplete(query); + emit(state.copyWith( + suggestions: suggestions, + isLoading: false, + )); + } + + void reset() { + emit(state.copyWith( + view: SearchView.suggestions, + suggestions: [], + isLoading: false, + )); + } + + @override + DocumentSearchState? fromJson(Map json) { + return DocumentSearchState.fromJson(json); + } + + @override + Map? toJson(DocumentSearchState state) { + return state.toJson(); + } +} diff --git a/lib/features/document_search/cubit/document_search_state.dart b/lib/features/search/cubit/document_search_state.dart similarity index 79% rename from lib/features/document_search/cubit/document_search_state.dart rename to lib/features/search/cubit/document_search_state.dart index 4286fb5..6667d7f 100644 --- a/lib/features/document_search/cubit/document_search_state.dart +++ b/lib/features/search/cubit/document_search_state.dart @@ -1,17 +1,25 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; part 'document_search_state.g.dart'; +enum SearchView { + suggestions, + results; +} + @JsonSerializable(ignoreUnannotated: true) -class DocumentSearchState extends DocumentsPagedState { +class DocumentSearchState extends PagedDocumentsState { @JsonKey() final List searchHistory; - + final SearchView view; + final List suggestions; const DocumentSearchState({ + this.view = SearchView.suggestions, this.searchHistory = const [], + this.suggestions = const [], super.filter, super.hasLoaded, super.isLoading, @@ -25,6 +33,8 @@ class DocumentSearchState extends DocumentsPagedState { filter, value, searchHistory, + suggestions, + view, ]; @override @@ -49,6 +59,7 @@ class DocumentSearchState extends DocumentsPagedState { List>? value, DocumentFilter? filter, List? suggestions, + SearchView? view, }) { return DocumentSearchState( value: value ?? this.value, @@ -56,6 +67,8 @@ class DocumentSearchState extends DocumentsPagedState { hasLoaded: hasLoaded ?? this.hasLoaded, isLoading: isLoading ?? this.isLoading, searchHistory: searchHistory ?? this.searchHistory, + view: view ?? this.view, + suggestions: suggestions ?? this.suggestions, ); } diff --git a/lib/features/document_search/cubit/document_search_state.g.dart b/lib/features/search/cubit/document_search_state.g.dart similarity index 100% rename from lib/features/document_search/cubit/document_search_state.g.dart rename to lib/features/search/cubit/document_search_state.g.dart diff --git a/lib/features/search/view/document_search_page.dart b/lib/features/search/view/document_search_page.dart new file mode 100644 index 0000000..2bedd3d --- /dev/null +++ b/lib/features/search/view/document_search_page.dart @@ -0,0 +1,166 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; +import 'package:paperless_mobile/features/search/cubit/document_search_state.dart'; +import 'package:paperless_mobile/features/search/cubit/document_search_cubit.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +Future showDocumentSearchPage(BuildContext context) { + return Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => DocumentSearchCubit(context.read()), + child: const DocumentSearchPage(), + ), + ), + ); +} + +class DocumentSearchPage extends StatefulWidget { + const DocumentSearchPage({super.key}); + + @override + State createState() => _DocumentSearchPageState(); +} + +class _DocumentSearchPageState extends State { + final _queryController = TextEditingController(text: ''); + + String get query => _queryController.text; + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + backgroundColor: theme.colorScheme.surface, + toolbarHeight: 72, + leading: BackButton( + color: theme.colorScheme.onSurface, + ), + title: TextField( + autofocus: true, + style: theme.textTheme.bodyLarge?.apply( + color: theme.colorScheme.onSurface, + ), + decoration: InputDecoration( + hintStyle: theme.textTheme.bodyLarge?.apply( + color: theme.colorScheme.onSurfaceVariant, + ), + hintText: "Search documents", + border: InputBorder.none, + ), + controller: _queryController, + onChanged: context.read().suggest, + onSubmitted: context.read().search, + ), + actions: [ + IconButton( + color: theme.colorScheme.onSurfaceVariant, + icon: Icon(Icons.clear), + onPressed: () { + context.read().reset(); + _queryController.clear(); + }, + ) + ], + bottom: PreferredSize( + preferredSize: Size.fromHeight(1), + child: Divider( + color: theme.colorScheme.outline, + ), + ), + ), + body: BlocBuilder( + builder: (context, state) { + switch (state.view) { + case SearchView.suggestions: + return _buildSuggestionsView(state); + case SearchView.results: + return _buildResultsView(state); + } + }, + ), + ); + } + + Widget _buildSuggestionsView(DocumentSearchState state) { + final suggestions = state.suggestions + .whereNot((element) => state.searchHistory.contains(element)) + .toList(); + final historyMatches = state.searchHistory + .where( + (element) => element.startsWith(query), + ) + .toList(); + return CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => ListTile( + title: Text(historyMatches[index]), + leading: Icon(Icons.history), + onTap: () => _selectSuggestion(historyMatches[index]), + ), + childCount: historyMatches.length, + ), + ), + if (state.isLoading) + const SliverToBoxAdapter( + child: Center( + child: CircularProgressIndicator(), + ), + ) + else + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => ListTile( + title: Text(suggestions[index]), + leading: Icon(Icons.search), + onTap: () => _selectSuggestion(suggestions[index]), + ), + childCount: suggestions.length, + ), + ) + ], + ); + } + + Widget _buildResultsView(DocumentSearchState state) { + final header = Text( + S.of(context).documentSearchResults, + style: Theme.of(context).textTheme.labelSmall, + ).padded(); + if (state.isLoading) { + return DocumentsListLoadingWidget( + beforeWidgets: [header], + ); + } + return CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: header), + if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) + SliverToBoxAdapter( + child: Center(child: Text("No documents found.")), + ) + else + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => DocumentListItem( + document: state.documents[index], + ), + childCount: state.documents.length, + ), + ), + ], + ); + } + + void _selectSuggestion(String suggestion) { + context.read().search(suggestion); + } +} diff --git a/lib/features/settings/bloc/application_settings_state.dart b/lib/features/settings/bloc/application_settings_state.dart index 7dfc48f..5771bdd 100644 --- a/lib/features/settings/bloc/application_settings_state.dart +++ b/lib/features/settings/bloc/application_settings_state.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; part 'application_settings_state.g.dart'; @@ -13,7 +14,7 @@ part 'application_settings_state.g.dart'; @JsonSerializable() class ApplicationSettingsState { static final defaultSettings = ApplicationSettingsState( - preferredLocaleSubtag: Platform.localeName.split('_').first, + preferredLocaleSubtag: _defaultPreferredLocaleSubtag, ); final bool isLocalAuthenticationEnabled; @@ -52,4 +53,13 @@ class ApplicationSettingsState { preferredColorSchemeOption ?? this.preferredColorSchemeOption, ); } + + static String get _defaultPreferredLocaleSubtag { + String preferredLocale = Platform.localeName.split("_").first; + if (!S.delegate.supportedLocales + .any((locale) => locale.languageCode == preferredLocale)) { + preferredLocale = 'en'; + } + return preferredLocale; + } } diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index 89db6ad..f29a4db 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -1,10 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; +import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart'; import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart'; import 'package:paperless_mobile/features/settings/view/pages/storage_settings_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -14,22 +26,58 @@ class SettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(S.of(context).appDrawerSettingsLabel), + actions: [ + IconButton( + icon: const Icon(Icons.logout), + color: Theme.of(context).colorScheme.error, + onPressed: () async { + await _onLogout(context); + Navigator.pop(context); + }, + ), + ], + ), + bottomNavigationBar: BlocBuilder( + builder: (context, state) { + final info = state.information!; + + return ListTile( + title: Text( + S.of(context).appDrawerHeaderLoggedInAsText + + " " + + (info.username ?? 'unknown') + + "@${info.host}", + style: Theme.of(context).textTheme.bodySmall, + ), + subtitle: Text( + S.of(context).serverInformationPaperlessVersionText + + ' ' + + info.version.toString() + + ' (API v${info.apiVersion})', + style: Theme.of(context).textTheme.bodySmall, + ), + ); + }, ), body: ListView( children: [ ListTile( + // leading: const Icon(Icons.style_outlined), title: Text(S.of(context).settingsPageApplicationSettingsLabel), subtitle: Text( S.of(context).settingsPageApplicationSettingsDescriptionText), onTap: () => _goto(const ApplicationSettingsPage(), context), ), ListTile( + // leading: const Icon(Icons.security_outlined), title: Text(S.of(context).settingsPageSecuritySettingsLabel), subtitle: Text(S.of(context).settingsPageSecuritySettingsDescriptionText), onTap: () => _goto(const SecuritySettingsPage(), context), ), ListTile( + // leading: const Icon(Icons.storage_outlined), title: Text(S.of(context).settingsPageStorageSettingsLabel), subtitle: Text(S.of(context).settingsPageStorageSettingsDescriptionText), @@ -52,4 +100,25 @@ class SettingsPage extends StatelessWidget { ), ); } + + Future _onLogout(BuildContext context) async { + try { + await context.read().logout(); + await context.read().clear(); + await context.read>().clear(); + await context + .read>() + .clear(); + await context + .read>() + .clear(); + await context + .read>() + .clear(); + await context.read().clear(); + await HydratedBloc.storage.clear(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } } diff --git a/lib/features/settings/view/widgets/language_selection_setting.dart b/lib/features/settings/view/widgets/language_selection_setting.dart index 38470e4..b141612 100644 --- a/lib/features/settings/view/widgets/language_selection_setting.dart +++ b/lib/features/settings/view/widgets/language_selection_setting.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart'; @@ -20,6 +21,7 @@ class _LanguageSelectionSettingState extends State { 'cs': 'Česky', 'tr': 'Türkçe', }; + @override Widget build(BuildContext context) { return BlocBuilder( @@ -27,9 +29,12 @@ class _LanguageSelectionSettingState extends State { return ListTile( title: Text(S.of(context).settingsPageLanguageSettingLabel), subtitle: Text(_languageOptions[settings.preferredLocaleSubtag]!), - onTap: () => showDialog( + onTap: () => showDialog( context: context, builder: (_) => RadioSettingsDialog( + footer: const Text( + "* Work in progress, not fully translated yet. Some words may be displayed in English!", + ), titleText: S.of(context).settingsPageLanguageSettingLabel, options: [ RadioOption( @@ -42,11 +47,11 @@ class _LanguageSelectionSettingState extends State { ), RadioOption( value: 'cs', - label: _languageOptions['cs']!, + label: _languageOptions['cs']! + " *", ), RadioOption( value: 'tr', - label: _languageOptions['tr']!, + label: _languageOptions['tr']! + " *", ) ], initialValue: context diff --git a/lib/features/similar_documents/cubit/similar_documents_cubit.dart b/lib/features/similar_documents/cubit/similar_documents_cubit.dart index 13dd481..dbaee29 100644 --- a/lib/features/similar_documents/cubit/similar_documents_cubit.dart +++ b/lib/features/similar_documents/cubit/similar_documents_cubit.dart @@ -1,12 +1,12 @@ import 'package:bloc/bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; -import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; part 'similar_documents_state.dart'; class SimilarDocumentsCubit extends Cubit - with DocumentsPagingMixin { + with PagedDocumentsMixin { final int documentId; @override diff --git a/lib/features/similar_documents/cubit/similar_documents_state.dart b/lib/features/similar_documents/cubit/similar_documents_state.dart index 4c4c664..75b683e 100644 --- a/lib/features/similar_documents/cubit/similar_documents_state.dart +++ b/lib/features/similar_documents/cubit/similar_documents_state.dart @@ -1,6 +1,6 @@ part of 'similar_documents_cubit.dart'; -class SimilarDocumentsState extends DocumentsPagedState { +class SimilarDocumentsState extends PagedDocumentsState { const SimilarDocumentsState({ super.filter, super.hasLoaded, diff --git a/lib/helpers/format_helpers.dart b/lib/helpers/format_helpers.dart index 5b863c4..d93ca57 100644 --- a/lib/helpers/format_helpers.dart +++ b/lib/helpers/format_helpers.dart @@ -4,7 +4,7 @@ String formatMaxCount(int? count, [int maxCount = 99]) { if ((count ?? 0) > maxCount) { return "$maxCount+"; } - return (count ?? 0).toString().padLeft(maxCount.toString().length); + return (count ?? 0).toString(); } String formatBytes(int bytes, int decimals) { From 9037af5bc6710b6a3ddc3e82769e064de58ab634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Luca=20Lagm=C3=B6ller?= Date: Mon, 30 Jan 2023 22:06:53 +0100 Subject: [PATCH 11/25] [BUGFIX] Added support for high refresh rate --- ios/Runner.xcodeproj/project.pbxproj | 5 +++- ios/Runner/Info.plist | 4 ++- lib/main.dart | 7 ++++- pubspec.lock | 40 +++++++++++++++++----------- pubspec.yaml | 1 + 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 664648c..1653b15 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -321,10 +321,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -335,6 +337,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6cb665f..a837d1a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -65,5 +65,7 @@ CADisableMinimumFrameDurationOnPhone - + UIApplicationSupportsIndirectInputEvents + + diff --git a/lib/main.dart b/lib/main.dart index 06e69ca..268620e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm; +import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; @@ -134,7 +135,11 @@ void main() async { //Update language header in interceptor on language change. appSettingsCubit.stream.listen((event) => languageHeaderInterceptor .preferredLocaleSubtag = event.preferredLocaleSubtag); - + + // Temporary Fix: Can be removed if the flutter engine implements the fix itself + // Activate the highest availabe refresh rate on the device + await FlutterDisplayMode.setHighRefreshRate(); + runApp( MultiProvider( providers: [ diff --git a/pubspec.lock b/pubspec.lock index 1ae465a..046cb7a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: "direct main" description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" connectivity_plus: dependency: "direct main" description: @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: coverage - sha256: d2494157c32b303f47dedee955b1479f2979c4ff66934eb7c0def44fd9e0267a + sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "1.6.2" cross_file: dependency: transitive description: @@ -519,6 +519,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + flutter_displaymode: + dependency: "direct main" + description: + name: flutter_displaymode + sha256: "136b0314fdc78fe995b0b75061fe9ff8210dffca84f8f8110f8f71029479db3b" + url: "https://pub.dev" + source: hosted + version: "0.5.0" flutter_driver: dependency: transitive description: flutter @@ -825,10 +833,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -1557,26 +1565,26 @@ packages: dependency: transitive description: name: test - sha256: "98403d1090ac0aa9e33dfc8bf45cc2e0c1d5c58d7cb832cee1e50bf14f37961d" + sha256: b54d427664c00f2013ffb87797a698883c46aee9288e027a50b46eaee7486fa2 url: "https://pub.dev" source: hosted - version: "1.22.1" + version: "1.22.2" test_api: dependency: transitive description: name: test_api - sha256: c9282698e2982b6c3817037554e52f99d4daba493e8028f8112a83d68ccd0b12 + sha256: "6182294da5abf431177fccc1ee02401f6df30f766bc6130a0852c6b6d7ee6b2d" url: "https://pub.dev" source: hosted - version: "0.4.17" + version: "0.4.18" test_core: dependency: transitive description: name: test_core - sha256: c9e4661a5e6285b795d47ba27957ed8b6f980fc020e98b218e276e88aff02168 + sha256: "95ecc12692d0dd59080ab2d38d9cf32c7e9844caba23ff6cd285690398ee8ef4" url: "https://pub.dev" source: hosted - version: "0.4.21" + version: "0.4.22" timezone: dependency: transitive description: @@ -1701,10 +1709,10 @@ packages: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + sha256: "2277c73618916ae3c2082b6df67b6ebb64b4c69d9bf23b23700707952ac30e60" url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "10.1.2" watcher: dependency: transitive description: @@ -1725,10 +1733,10 @@ packages: dependency: transitive description: name: webdriver - sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index eb18cd7..25d5e71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,7 @@ dependencies: flutter_staggered_grid_view: ^0.6.2 responsive_builder: ^0.4.3 open_filex: ^4.3.2 + flutter_displaymode: ^0.5.0 dev_dependencies: integration_test: From e9e9fdc336fa35ea2884ffe3a46aad516d078af2 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 31 Jan 2023 00:29:07 +0100 Subject: [PATCH 12/25] Improved search, changed saved view display --- lib/features/app_drawer/view/app_drawer.dart | 0 .../view/pages/similar_documents_view.dart | 53 +- .../documents/bloc/documents_cubit.dart | 27 - .../documents/bloc/documents_state.dart | 10 - .../documents/view/pages/documents_page.dart | 536 ++++++++---------- .../view/widgets/adaptive_documents_view.dart | 232 ++++++++ .../widgets/document_grid_loading_widget.dart | 0 .../view/widgets/documents_empty_state.dart | 6 +- .../documents_list_loading_widget.dart | 28 +- .../{grid => items}/document_grid_item.dart | 44 +- .../view/widgets/items/document_item.dart | 32 ++ .../{list => items}/document_list_item.dart | 49 +- .../widgets/list/adaptive_documents_view.dart | 114 ---- .../widgets/search/document_filter_form.dart | 214 +++++++ .../widgets/search/document_filter_panel.dart | 244 ++------ .../documents/view/widgets/view_actions.dart | 39 ++ lib/features/inbox/view/pages/inbox_page.dart | 2 +- .../view/pages/linked_documents_page.dart | 25 +- .../cubit/saved_view_details_cubit.dart | 20 + .../cubit/saved_view_details_state.dart | 47 ++ .../saved_view/view/add_saved_view_page.dart | 125 ++-- .../saved_view/view/saved_view_list.dart | 61 ++ .../saved_view/view/saved_view_page.dart | 133 +++++ .../view/saved_view_selection_widget.dart | 420 +++++++------- .../search/view/document_search_page.dart | 50 +- .../search/view/documents_search_app_bar.dart | 1 + .../search_app_bar/view/search_app_bar.dart | 53 ++ 27 files changed, 1549 insertions(+), 1016 deletions(-) create mode 100644 lib/features/app_drawer/view/app_drawer.dart create mode 100644 lib/features/documents/view/widgets/adaptive_documents_view.dart create mode 100644 lib/features/documents/view/widgets/document_grid_loading_widget.dart rename lib/{core => features/documents/view}/widgets/documents_list_loading_widget.dart (80%) rename lib/features/documents/view/widgets/{grid => items}/document_grid_item.dart (78%) create mode 100644 lib/features/documents/view/widgets/items/document_item.dart rename lib/features/documents/view/widgets/{list => items}/document_list_item.dart (66%) delete mode 100644 lib/features/documents/view/widgets/list/adaptive_documents_view.dart create mode 100644 lib/features/documents/view/widgets/search/document_filter_form.dart create mode 100644 lib/features/documents/view/widgets/view_actions.dart create mode 100644 lib/features/saved_view/cubit/saved_view_details_cubit.dart create mode 100644 lib/features/saved_view/cubit/saved_view_details_state.dart create mode 100644 lib/features/saved_view/view/saved_view_list.dart create mode 100644 lib/features/saved_view/view/saved_view_page.dart create mode 100644 lib/features/search/view/documents_search_app_bar.dart create mode 100644 lib/features/search_app_bar/view/search_app_bar.dart diff --git a/lib/features/app_drawer/view/app_drawer.dart b/lib/features/app_drawer/view/app_drawer.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/document_details/view/pages/similar_documents_view.dart b/lib/features/document_details/view/pages/similar_documents_view.dart index 2fb2b53..01c5fa2 100644 --- a/lib/features/document_details/view/pages/similar_documents_view.dart +++ b/lib/features/document_details/view/pages/similar_documents_view.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/constants.dart'; @@ -58,11 +60,6 @@ class _SimilarDocumentsViewState extends State { ); return BlocBuilder( builder: (context, state) { - if (!state.hasLoaded) { - return const DocumentsListLoadingWidget( - beforeWidgets: [earlyPreviewHintCard], - ); - } if (state.documents.isEmpty) { return DocumentsEmptyState( state: state, @@ -74,20 +71,36 @@ class _SimilarDocumentsViewState extends State { ), ); } - return CustomScrollView( - controller: _scrollController, - slivers: [ - const SliverToBoxAdapter(child: earlyPreviewHintCard), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: state.documents.length, - (context, index) => DocumentListItem( - document: state.documents[index], - enableHeroAnimation: false, + + return BlocBuilder( + builder: (context, connectivity) { + return CustomScrollView( + controller: _scrollController, + slivers: [ + const SliverToBoxAdapter(child: earlyPreviewHintCard), + SliverAdaptiveDocumentsView( + documents: state.documents, + hasInternetConnection: connectivity.isConnected, + isLabelClickable: false, + isLoading: state.isLoading, + hasLoaded: state.hasLoaded, + ), - ), - ), - ], + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: state.documents.length, + (context, index) => DocumentListItem( + document: state.documents[index], + enableHeroAnimation: false, + isLabelClickable: false, + isSelected: false, + isSelectionActive: false, + ), + ), + ), + ], + ); + }, ); }, ); diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 428fb91..eefd2c6 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -66,38 +66,11 @@ class DocumentsCubit extends HydratedCubit emit(const DocumentsState()); } - Future selectView(int id) async { - emit(state.copyWith(isLoading: true)); - try { - final filter = - _savedViewRepository.current?.values[id]?.toDocumentFilter(); - if (filter == null) { - return; - } - final results = await api.findAll(filter.copyWith(page: 1)); - emit( - DocumentsState( - filter: filter, - hasLoaded: true, - isLoading: false, - selectedSavedViewId: id, - value: [results], - ), - ); - } finally { - emit(state.copyWith(isLoading: false)); - } - } - Future> autocomplete(String query) async { final res = await api.autocomplete(query); return res; } - void unselectView() { - emit(state.copyWith(selectedSavedViewId: () => null)); - } - @override DocumentsState? fromJson(Map json) { return DocumentsState.fromJson(json); diff --git a/lib/features/documents/bloc/documents_state.dart b/lib/features/documents/bloc/documents_state.dart index 23adc83..1e080a5 100644 --- a/lib/features/documents/bloc/documents_state.dart +++ b/lib/features/documents/bloc/documents_state.dart @@ -3,14 +3,11 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; class DocumentsState extends PagedDocumentsState { - final int? selectedSavedViewId; - @JsonKey(ignore: true) final List selection; const DocumentsState({ this.selection = const [], - this.selectedSavedViewId, super.value = const [], super.filter = const DocumentFilter(), super.hasLoaded = false, @@ -25,7 +22,6 @@ class DocumentsState extends PagedDocumentsState { List>? value, DocumentFilter? filter, List? selection, - int? Function()? selectedSavedViewId, }) { return DocumentsState( hasLoaded: hasLoaded ?? this.hasLoaded, @@ -33,9 +29,6 @@ class DocumentsState extends PagedDocumentsState { value: value ?? this.value, filter: filter ?? this.filter, selection: selection ?? this.selection, - selectedSavedViewId: selectedSavedViewId != null - ? selectedSavedViewId.call() - : this.selectedSavedViewId, ); } @@ -46,7 +39,6 @@ class DocumentsState extends PagedDocumentsState { value, selection, isLoading, - selectedSavedViewId, ]; Map toJson() { @@ -54,7 +46,6 @@ class DocumentsState extends PagedDocumentsState { 'hasLoaded': hasLoaded, 'isLoading': isLoading, 'filter': filter.toJson(), - 'selectedSavedViewId': selectedSavedViewId, 'value': value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(), }; @@ -65,7 +56,6 @@ class DocumentsState extends PagedDocumentsState { return DocumentsState( hasLoaded: json['hasLoaded'], isLoading: json['isLoading'], - selectedSavedViewId: json['selectedSavedViewId'], value: (json['value'] as List) .map((e) => PagedSearchResult.fromJsonT(e, DocumentModelJsonConverter())) diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 0777db5..8f8c7ef 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:badges/badges.dart' as b; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -5,35 +7,26 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart'; -import 'package:paperless_mobile/core/widgets/app_options_popup_menu.dart'; -import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart'; -import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; -import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; -import 'package:paperless_mobile/features/document_search/document_search_delegate.dart'; -import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; -import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/view_actions.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart'; -import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; -import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart'; +import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; +import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart'; import 'package:paperless_mobile/features/search/view/document_search_page.dart'; +import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; -import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; class DocumentFilterIntent { @@ -46,6 +39,7 @@ class DocumentFilterIntent { }); } +//TODO: Refactor this class DocumentsPage extends StatefulWidget { const DocumentsPage({Key? key}) : super(key: key); @@ -53,56 +47,38 @@ class DocumentsPage extends StatefulWidget { State createState() => _DocumentsPageState(); } -class _DocumentsPageState extends State { - final ScrollController _scrollController = ScrollController(); - double _offset = 0; - double _last = 0; +class _DocumentsPageState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController; - static const double _savedViewWidgetHeight = 80 + 16; + int _currentTab = 0; @override void initState() { super.initState(); + _tabController = TabController( + length: 2, + vsync: this, + initialIndex: 0, + ); try { context.read().reload(); context.read().reload(); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } - _scrollController - ..addListener(_listenForScrollChanges) - ..addListener(_listenForLoadNewData); + _tabController.addListener(_listenForTabChanges); } - void _listenForLoadNewData() async { - final currState = context.read().state; - if (_scrollController.offset >= - _scrollController.position.maxScrollExtent * 0.75 && - !currState.isLoading && - !currState.isLastPageLoaded) { - try { - await context.read().loadMore(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - } - - void _listenForScrollChanges() { - final current = _scrollController.offset; - _offset += _last - current; - - if (_offset <= -_savedViewWidgetHeight) _offset = -_savedViewWidgetHeight; - if (_offset >= 0) _offset = 0; - _last = current; - if (_offset <= 0 && _offset >= -_savedViewWidgetHeight) { - setState(() {}); - } + void _listenForTabChanges() { + setState(() { + _currentTab = _tabController.index; + }); } @override void dispose() { - _scrollController.dispose(); + _tabController.dispose(); super.dispose(); } @@ -140,149 +116,204 @@ class _DocumentsPageState extends State { }, builder: (context, connectivityState) { return Scaffold( - // appBar: PreferredSize( - // preferredSize: const Size.fromHeight( - // kToolbarHeight, - // ), - // child: BlocBuilder( - // builder: (context, state) { - // if (state.selection.isEmpty) { - // return DocumentSearchBar(); - // // return AppBar( - // // title: Text(S.of(context).documentsPageTitle + - // // " (${formatMaxCount(state.documents.length)})"), - // // actions: [ - // // IconButton( - // // icon: const Icon(Icons.search), - // // onPressed: () { - // // showMaterial3Search( - // // context: context, - // // delegate: DocumentSearchDelegate( - // // DocumentSearchCubit(context.read()), - // // searchFieldStyle: - // // Theme.of(context).textTheme.bodyLarge, - // // hintText: "Search documents", //TODO: INTL - // // ), - // // ); - // // }, - // // ), - // // const SortDocumentsButton(), - // // const AppOptionsPopupMenu( - // // displayedActions: [ - // // AppPopupMenuEntries.documentsSelectListView, - // // AppPopupMenuEntries.documentsSelectGridView, - // // AppPopupMenuEntries.divider, - // // AppPopupMenuEntries.openAboutThisAppDialog, - // // AppPopupMenuEntries.reportBug, - // // AppPopupMenuEntries.openSettings, - // // ], - // // ), - // // ], - // // ); - // } else { - // return AppBar( - // leading: IconButton( - // icon: const Icon(Icons.close), - // onPressed: () => - // context.read().resetSelection(), - // ), - // title: Text( - // '${state.selection.length} ${S.of(context).documentsSelectedText}'), - // actions: [ - // IconButton( - // icon: const Icon(Icons.delete), - // onPressed: () => _onDelete(context, state), - // ), - // ], - // ); - // } - // }, - // ), - // ), - // floatingActionButton: BlocBuilder( - // builder: (context, state) { - // final appliedFiltersCount = state.filter.appliedFiltersCount; - // return b.Badge( - // position: b.BadgePosition.topEnd(top: -12, end: -6), - // showBadge: appliedFiltersCount > 0, - // badgeContent: Text( - // '$appliedFiltersCount', - // style: const TextStyle( - // color: Colors.white, - // ), - // ), - // animationType: b.BadgeAnimationType.fade, - // badgeColor: Colors.red, - // child: FloatingActionButton( - // child: const Icon(Icons.filter_alt_outlined), - // onPressed: _openDocumentFilter, - // ), - // ); - // }, - // ), + floatingActionButton: BlocBuilder( + builder: (context, state) { + final appliedFiltersCount = state.filter.appliedFiltersCount; + return b.Badge( + position: b.BadgePosition.topEnd(top: -12, end: -6), + showBadge: appliedFiltersCount > 0, + badgeContent: Text( + '$appliedFiltersCount', + style: const TextStyle( + color: Colors.white, + ), + ), + animationType: b.BadgeAnimationType.fade, + badgeColor: Colors.red, + child: _currentTab == 0 + ? FloatingActionButton( + child: const Icon(Icons.filter_alt_outlined), + onPressed: _openDocumentFilter, + ) + : FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () => _onCreateSavedView(state.filter), + ), + ); + }, + ), resizeToAvoidBottomInset: true, - body: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverAppBar( - floating: true, - pinned: true, - snap: true, - title: SearchBar( - height: kToolbarHeight - 2, - supportingText: "Search documents", - onTap: () { - showDocumentSearchPage(context); - }, - leadingIcon: Icon(Icons.menu), - trailingIcon: CircleAvatar( - child: Text("A"), + body: WillPopScope( + onWillPop: () async { + if (context.read().state.selection.isNotEmpty) { + context.read().resetSelection(); + } + return false; + }, + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + // This widget takes the overlapping behavior of the SliverAppBar, + // and redirects it to the SliverOverlapInjector below. If it is + // missing, then it is possible for the nested "inner" scroll view + // below to end up under the SliverAppBar even when the inner + // scroll view thinks it has not been scrolled. + // This is not necessary if the "headerSliverBuilder" only builds + // widgets that do not overlap the next sliver. + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + sliver: SearchAppBar( + onOpenSearch: showDocumentSearchPage, + bottom: TabBar( + controller: _tabController, + isScrollable: true, + tabs: [ + Tab(text: S.of(context).documentsPageTitle), + Tab(text: S.of(context).savedViewsLabel), + ], ), ), - ) - ]; - }, - body: WillPopScope( - onWillPop: () async { - if (context - .read() - .state - .selection - .isNotEmpty) { - context.read().resetSelection(); - } - return false; - }, - child: RefreshIndicator( - onRefresh: _onRefresh, - notificationPredicate: (_) => connectivityState.isConnected, - child: BlocBuilder( - builder: (context, taskState) { - return _buildBody(connectivityState); - // return Stack( - // children: [ - // Positioned( - // left: 0, - // right: 0, - // top: _offset, - // child: BlocBuilder( - // builder: (context, state) { - // return ColoredBox( - // color: - // Theme.of(context).colorScheme.background, - // child: SavedViewSelectionWidget( - // height: _savedViewWidgetHeight, - // currentFilter: state.filter, - // enabled: state.selection.isEmpty && - // connectivityState.isConnected, - // ), - // ); - // }, - // ), - // ), - // ], - // ); + ), + ], + body: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + final desiredTab = + (metrics.pixels / metrics.maxScrollExtent).round(); + if (metrics.axis == Axis.horizontal && + _currentTab != desiredTab) { + setState(() => _currentTab = desiredTab); + } + return true; + }, + child: NotificationListener( + onNotification: (notification) { + // Listen for scroll notifications to load new data. + // Scroll controller does not work here due to nestedscrollview limitations. + final currState = context.read().state; + final max = notification.metrics.maxScrollExtent; + if (max == 0 || + _currentTab != 0 || + currState.isLoading || + currState.isLastPageLoaded) { + return true; + } + final offset = notification.metrics.pixels; + if (offset >= max * 0.7) { + context + .read() + .loadMore() + .onError( + (error, stackTrace) => showErrorMessage( + context, + error, + stackTrace, + ), + ); + } + return true; }, + child: TabBarView( + controller: _tabController, + children: [ + Builder( + builder: (context) { + return RefreshIndicator( + edgeOffset: kToolbarHeight, + onRefresh: _onReloadDocuments, + notificationPredicate: (_) => + connectivityState.isConnected, + child: CustomScrollView( + key: const PageStorageKey("documents"), + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor( + context), + ), + BlocBuilder( + buildWhen: (previous, current) => + !const ListEquality().equals( + previous.documents, + current.documents, + ) || + previous.selectedIds != + current.selectedIds, + builder: (context, state) { + if (state.hasLoaded && + state.documents.isEmpty) { + return SliverToBoxAdapter( + child: DocumentsEmptyState( + state: state, + onReset: () { + context + .read() + .resetFilter(); + }, + ), + ); + } + return BlocBuilder< + ApplicationSettingsCubit, + ApplicationSettingsState>( + builder: (context, settings) { + return SliverAdaptiveDocumentsView( + viewType: + settings.preferredViewType, + onTap: _openDetails, + onSelected: context + .read() + .toggleDocumentSelection, + hasInternetConnection: + connectivityState.isConnected, + onTagSelected: _addTagToFilter, + onCorrespondentSelected: + _addCorrespondentToFilter, + onDocumentTypeSelected: + _addDocumentTypeToFilter, + onStoragePathSelected: + _addStoragePathToFilter, + documents: state.documents, + hasLoaded: state.hasLoaded, + isLabelClickable: true, + isLoading: state.isLoading, + selectedDocumentIds: + state.selectedIds, + ); + }, + ); + }, + ), + ], + ), + ); + }, + ), + Builder( + builder: (context) { + return RefreshIndicator( + edgeOffset: kToolbarHeight, + onRefresh: _onReloadSavedViews, + notificationPredicate: (_) => + connectivityState.isConnected, + child: CustomScrollView( + key: const PageStorageKey("savedViews"), + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor( + context), + ), + const SavedViewList(), + ], + ), + ); + }, + ), + ], + ), ), ), ), @@ -293,28 +324,7 @@ class _DocumentsPageState extends State { ); } - BlocBuilder - _buildViewTypeButton() { - return BlocBuilder( - builder: (context, settingsState) => IconButton( - icon: Icon( - settingsState.preferredViewType == ViewType.grid - ? Icons.list - : Icons.grid_view_rounded, - ), - onPressed: () { - // Reset saved view widget position as scroll offset will be reset anyway. - setState(() { - _offset = 0; - _last = 0; - }); - final cubit = context.read(); - cubit.setViewType(cubit.state.preferredViewType.toggle()); - }, - ), - ); - } - + //TODO: Add app bar... void _onDelete(BuildContext context, DocumentsState documentsState) async { final shouldDelete = await showDialog( context: context, @@ -338,6 +348,25 @@ class _DocumentsPageState extends State { } } + void _onCreateSavedView(DocumentFilter filter) async { + final newView = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LabelsBlocProvider( + child: AddSavedViewPage( + currentFilter: filter, + ), + ), + ), + ); + if (newView != null) { + try { + await context.read().add(newView); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + } + void _openDocumentFilter() async { final draggableSheetController = DraggableScrollableController(); final filterIntent = await showModalBottomSheet( @@ -373,12 +402,7 @@ class _DocumentsPageState extends State { try { if (filterIntent.shouldReset) { await context.read().resetFilter(); - context.read().unselectView(); } else { - if (filterIntent.filter != - context.read().state.filter) { - context.read().unselectView(); - } await context .read() .updateFilter(filter: filterIntent.filter!); @@ -389,75 +413,11 @@ class _DocumentsPageState extends State { } } - String _formatDocumentCount(int count) { - return count > 99 ? "99+" : count.toString(); - } - - Widget _buildBody(ConnectivityState connectivityState) { - final isConnected = connectivityState == ConnectivityState.connected; - return BlocBuilder( - builder: (context, settings) { - return BlocBuilder( - buildWhen: (previous, current) => - !const ListEquality() - .equals(previous.documents, current.documents) || - previous.selectedIds != current.selectedIds, - builder: (context, state) { - if (state.hasLoaded && state.documents.isEmpty) { - return DocumentsEmptyState( - state: state, - onReset: () { - context.read().resetFilter(); - context.read().unselectView(); - }, - ); - } - - return AdaptiveDocumentsView( - viewType: settings.preferredViewType, - state: state, - scrollController: _scrollController, - onTap: _openDetails, - onSelected: _onSelected, - hasInternetConnection: isConnected, - onTagSelected: _addTagToFilter, - onCorrespondentSelected: _addCorrespondentToFilter, - onDocumentTypeSelected: _addDocumentTypeToFilter, - onStoragePathSelected: _addStoragePathToFilter, - pageLoadingWidget: const NewItemsLoadingWidget(), - beforeItems: SizedBox( - height: kToolbarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SortDocumentsButton(), - IconButton( - icon: Icon( - settings.preferredViewType == ViewType.grid - ? Icons.list - : Icons.grid_view_rounded, - ), - onPressed: () => - context.read().setViewType( - settings.preferredViewType.toggle(), - ), - ), - ], - ), - ), - ); - }, - ); - }, - ); - } - Future _openDetails(DocumentModel document) async { - final potentiallyUpdatedModel = - await Navigator.of(context).push( + final updatedModel = await Navigator.of(context).push( _buildDetailsPageRoute(document), ); - if (potentiallyUpdatedModel != document) { + if (updatedModel != document) { context.read().reload(); } } @@ -558,15 +518,19 @@ class _DocumentsPageState extends State { } } - void _onSelected(DocumentModel model) { - context.read().toggleDocumentSelection(model); - } - - Future _onRefresh() async { + Future _onReloadDocuments() async { try { // We do not await here on purpose so we can show a linear progress indicator below the app bar. - context.read().reload(); - context.read().reload(); + await context.read().reload(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + + Future _onReloadSavedViews() async { + try { + // We do not await here on purpose so we can show a linear progress indicator below the app bar. + await context.read().reload(); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } diff --git a/lib/features/documents/view/widgets/adaptive_documents_view.dart b/lib/features/documents/view/widgets/adaptive_documents_view.dart new file mode 100644 index 0000000..ee1d343 --- /dev/null +++ b/lib/features/documents/view/widgets/adaptive_documents_view.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; + +abstract class AdaptiveDocumentsView extends StatelessWidget { + final List documents; + final bool isLoading; + final bool hasLoaded; + final bool enableHeroAnimation; + final List selectedDocumentIds; + final ViewType viewType; + final void Function(DocumentModel)? onTap; + final void Function(DocumentModel)? onSelected; + final bool hasInternetConnection; + final bool isLabelClickable; + final void Function(int id)? onTagSelected; + final void Function(int? id)? onCorrespondentSelected; + final void Function(int? id)? onDocumentTypeSelected; + final void Function(int? id)? onStoragePathSelected; + + const AdaptiveDocumentsView({ + super.key, + this.selectedDocumentIds = const [], + required this.documents, + this.onTap, + this.onSelected, + this.viewType = ViewType.list, + required this.hasInternetConnection, + required this.isLabelClickable, + this.onTagSelected, + this.onCorrespondentSelected, + this.onDocumentTypeSelected, + this.onStoragePathSelected, + required this.isLoading, + required this.hasLoaded, + this.enableHeroAnimation = true, + }); +} + +class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { + const SliverAdaptiveDocumentsView({ + super.key, + required super.documents, + required super.hasInternetConnection, + required super.isLabelClickable, + super.onCorrespondentSelected, + super.onDocumentTypeSelected, + super.onStoragePathSelected, + super.onSelected, + super.onTagSelected, + super.onTap, + super.selectedDocumentIds, + super.viewType, + required super.isLoading, + required super.hasLoaded, + }); + + @override + Widget build(BuildContext context) { + switch (viewType) { + case ViewType.grid: + return _buildGridView(); + case ViewType.list: + return _buildListView(); + } + } + + Widget _buildListView() { + if (!hasLoaded && isLoading) { + return const DocumentsListLoadingWidget(); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + childCount: documents.length, + (context, index) { + final document = documents.elementAt(index); + return LabelRepositoriesProvider( + child: DocumentListItem( + isLabelClickable: isLabelClickable, + document: document, + onTap: onTap, + isSelected: selectedDocumentIds.contains(document.id), + onSelected: onSelected, + isSelectionActive: selectedDocumentIds.isNotEmpty, + onTagSelected: onTagSelected, + onCorrespondentSelected: onCorrespondentSelected, + onDocumentTypeSelected: onDocumentTypeSelected, + onStoragePathSelected: onStoragePathSelected, + ), + ); + }, + ), + ); + } + + Widget _buildGridView() { + if (!hasLoaded && isLoading) { + return const DocumentsListLoadingWidget(); + } + return SliverGrid.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + childAspectRatio: 1 / 2, + ), + itemCount: documents.length, + itemBuilder: (context, index) { + final document = documents.elementAt(index); + return DocumentGridItem( + document: document, + onTap: onTap, + isSelected: selectedDocumentIds.contains(document.id), + onSelected: onSelected, + isSelectionActive: selectedDocumentIds.isNotEmpty, + isLabelClickable: isLabelClickable, + onTagSelected: onTagSelected, + onCorrespondentSelected: onCorrespondentSelected, + onDocumentTypeSelected: onDocumentTypeSelected, + onStoragePathSelected: onStoragePathSelected, + enableHeroAnimation: enableHeroAnimation, + ); + }, + ); + } +} + +class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { + final ScrollController? scrollController; + const DefaultAdaptiveDocumentsView({ + super.key, + required super.documents, + required super.hasInternetConnection, + required super.isLabelClickable, + required super.isLoading, + required super.hasLoaded, + super.onCorrespondentSelected, + super.onDocumentTypeSelected, + super.onStoragePathSelected, + super.onSelected, + super.onTagSelected, + super.onTap, + this.scrollController, + super.selectedDocumentIds, + super.viewType, + super.enableHeroAnimation = true, + }); + + @override + Widget build(BuildContext context) { + switch (viewType) { + case ViewType.grid: + return _buildGridView(); + case ViewType.list: + return _buildListView(); + } + } + + Widget _buildListView() { + if (!hasLoaded && isLoading) { + return const CustomScrollView(slivers: [ + DocumentsListLoadingWidget(), + ]); + } + + return ListView.builder( + controller: scrollController, + primary: false, + itemCount: documents.length, + itemBuilder: (context, index) { + final document = documents.elementAt(index); + return LabelRepositoriesProvider( + child: DocumentListItem( + isLabelClickable: isLabelClickable, + document: document, + onTap: onTap, + isSelected: selectedDocumentIds.contains(document.id), + onSelected: onSelected, + isSelectionActive: selectedDocumentIds.isNotEmpty, + onTagSelected: onTagSelected, + onCorrespondentSelected: onCorrespondentSelected, + onDocumentTypeSelected: onDocumentTypeSelected, + onStoragePathSelected: onStoragePathSelected, + enableHeroAnimation: enableHeroAnimation, + ), + ); + }, + ); + } + + Widget _buildGridView() { + if (!hasLoaded && isLoading) { + return const CustomScrollView( + slivers: [ + DocumentsListLoadingWidget(), + ], + ); //TODO: Build grid skeleton + } + return GridView.builder( + controller: scrollController, + primary: false, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + childAspectRatio: 1 / 2, + ), + itemCount: documents.length, + itemBuilder: (context, index) { + final document = documents.elementAt(index); + return DocumentGridItem( + document: document, + onTap: onTap, + isSelected: selectedDocumentIds.contains(document.id), + onSelected: onSelected, + isSelectionActive: selectedDocumentIds.isNotEmpty, + isLabelClickable: isLabelClickable, + onTagSelected: onTagSelected, + onCorrespondentSelected: onCorrespondentSelected, + onDocumentTypeSelected: onDocumentTypeSelected, + onStoragePathSelected: onStoragePathSelected, + enableHeroAnimation: enableHeroAnimation, + ); + }, + ); + } +} diff --git a/lib/features/documents/view/widgets/document_grid_loading_widget.dart b/lib/features/documents/view/widgets/document_grid_loading_widget.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/documents/view/widgets/documents_empty_state.dart b/lib/features/documents/view/widgets/documents_empty_state.dart index 9612fc7..4c3d25b 100644 --- a/lib/features/documents/view/widgets/documents_empty_state.dart +++ b/lib/features/documents/view/widgets/documents_empty_state.dart @@ -7,11 +7,11 @@ import 'package:paperless_mobile/generated/l10n.dart'; class DocumentsEmptyState extends StatelessWidget { final PagedDocumentsState state; - final VoidCallback onReset; + final VoidCallback? onReset; const DocumentsEmptyState({ Key? key, required this.state, - required this.onReset, + this.onReset, }) : super(key: key); @override @@ -20,7 +20,7 @@ class DocumentsEmptyState extends StatelessWidget { child: EmptyState( title: S.of(context).documentsPageEmptyStateOopsText, subtitle: S.of(context).documentsPageEmptyStateNothingHereText, - bottomChild: state.filter != DocumentFilter.initial + bottomChild: state.filter != DocumentFilter.initial && onReset != null ? TextButton( onPressed: onReset, child: Text( diff --git a/lib/core/widgets/documents_list_loading_widget.dart b/lib/features/documents/view/widgets/documents_list_loading_widget.dart similarity index 80% rename from lib/core/widgets/documents_list_loading_widget.dart rename to lib/features/documents/view/widgets/documents_list_loading_widget.dart index 8d1575c..433a607 100644 --- a/lib/core/widgets/documents_list_loading_widget.dart +++ b/lib/features/documents/view/widgets/documents_list_loading_widget.dart @@ -5,37 +5,23 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:shimmer/shimmer.dart'; class DocumentsListLoadingWidget extends StatelessWidget { - final List beforeWidgets; - final List afterWidgets; - static const _tags = [" ", " ", " "]; static const _titleLengths = [double.infinity, 150.0, 200.0]; static const _correspondentLengths = [200.0, 300.0, 150.0]; static const _fontSize = 16.0; - const DocumentsListLoadingWidget({ - super.key, - this.beforeWidgets = const [], - this.afterWidgets = const [], + const DocumentsListLoadingWidget({super.key }); @override Widget build(BuildContext context) { final _random = Random(); - return CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildListDelegate(beforeWidgets), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return _buildFakeListItem(context, _random); - }, - ), - ), - SliverList(delegate: SliverChildListDelegate(afterWidgets)) - ], + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return _buildFakeListItem(context, _random); + }, + ), ); } diff --git a/lib/features/documents/view/widgets/grid/document_grid_item.dart b/lib/features/documents/view/widgets/items/document_grid_item.dart similarity index 78% rename from lib/features/documents/view/widgets/grid/document_grid_item.dart rename to lib/features/documents/view/widgets/items/document_grid_item.dart index 00a50be..4c494e8 100644 --- a/lib/features/documents/view/widgets/grid/document_grid_item.dart +++ b/lib/features/documents/view/widgets/items/document_grid_item.dart @@ -1,38 +1,35 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:intl/intl.dart'; -class DocumentGridItem extends StatelessWidget { - final DocumentModel document; - final bool isSelected; - final void Function(DocumentModel) onTap; - final void Function(DocumentModel) onSelected; - final bool isAtLeastOneSelected; - final bool Function(int tagId) isTagSelectedPredicate; - final void Function(int tagId)? onTagSelected; - +class DocumentGridItem extends DocumentItem { const DocumentGridItem({ - Key? key, - required this.document, - required this.onTap, - required this.onSelected, - required this.isSelected, - required this.isAtLeastOneSelected, - required this.isTagSelectedPredicate, - required this.onTagSelected, - }) : super(key: key); + super.key, + required super.document, + required super.isSelected, + required super.isSelectionActive, + required super.isLabelClickable, + super.onCorrespondentSelected, + super.onDocumentTypeSelected, + super.onSelected, + super.onStoragePathSelected, + super.onTagSelected, + super.onTap, + required super.enableHeroAnimation, + }); @override Widget build(BuildContext context) { return GestureDetector( onTap: _onTap, - onLongPress: () => onSelected(document), + onLongPress: onSelected != null ? () => onSelected!(document) : null, child: AbsorbPointer( - absorbing: isAtLeastOneSelected, + absorbing: isSelectionActive, child: Padding( padding: const EdgeInsets.all(8.0), child: Card( @@ -48,6 +45,7 @@ class DocumentGridItem extends StatelessWidget { child: DocumentPreview( id: document.id, borderRadius: 12.0, + enableHero: enableHeroAnimation, ), ), Expanded( @@ -94,10 +92,10 @@ class DocumentGridItem extends StatelessWidget { } void _onTap() { - if (isAtLeastOneSelected || isSelected) { - onSelected(document); + if (isSelectionActive || isSelected) { + onSelected?.call(document); } else { - onTap(document); + onTap?.call(document); } } } diff --git a/lib/features/documents/view/widgets/items/document_item.dart b/lib/features/documents/view/widgets/items/document_item.dart new file mode 100644 index 0000000..a19fef3 --- /dev/null +++ b/lib/features/documents/view/widgets/items/document_item.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_api/paperless_api.dart'; + +abstract class DocumentItem extends StatelessWidget { + final DocumentModel document; + final void Function(DocumentModel)? onTap; + final void Function(DocumentModel)? onSelected; + final bool isSelected; + final bool isSelectionActive; + final bool isLabelClickable; + final bool enableHeroAnimation; + + final void Function(int tagId)? onTagSelected; + final void Function(int? correspondentId)? onCorrespondentSelected; + final void Function(int? documentTypeId)? onDocumentTypeSelected; + final void Function(int? id)? onStoragePathSelected; + + const DocumentItem({ + super.key, + required this.document, + this.onTap, + this.onSelected, + required this.isSelected, + required this.isSelectionActive, + required this.isLabelClickable, + this.onTagSelected, + this.onCorrespondentSelected, + this.onDocumentTypeSelected, + this.onStoragePathSelected, + required this.enableHeroAnimation, + }); +} diff --git a/lib/features/documents/view/widgets/list/document_list_item.dart b/lib/features/documents/view/widgets/items/document_list_item.dart similarity index 66% rename from lib/features/documents/view/widgets/list/document_list_item.dart rename to lib/features/documents/view/widgets/items/document_list_item.dart index 116bf63..f63f165 100644 --- a/lib/features/documents/view/widgets/list/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -1,39 +1,26 @@ import 'package:flutter/material.dart'; -import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; -class DocumentListItem extends StatelessWidget { +class DocumentListItem extends DocumentItem { static const _a4AspectRatio = 1 / 1.4142; - final DocumentModel document; - final void Function(DocumentModel)? onTap; - final void Function(DocumentModel)? onSelected; - final bool isSelected; - final bool isAtLeastOneSelected; - final bool isLabelClickable; - - final void Function(int tagId)? onTagSelected; - final void Function(int? correspondentId)? onCorrespondentSelected; - final void Function(int? documentTypeId)? onDocumentTypeSelected; - final void Function(int? id)? onStoragePathSelected; - - final bool enableHeroAnimation; const DocumentListItem({ - Key? key, - required this.document, - this.onTap, - this.onSelected, - this.isSelected = false, - this.isAtLeastOneSelected = false, - this.isLabelClickable = true, - this.onTagSelected, - this.onCorrespondentSelected, - this.onDocumentTypeSelected, - this.onStoragePathSelected, - this.enableHeroAnimation = true, - }) : super(key: key); + super.key, + required super.document, + required super.isSelected, + required super.isSelectionActive, + required super.isLabelClickable, + super.onCorrespondentSelected, + super.onDocumentTypeSelected, + super.onSelected, + super.onStoragePathSelected, + super.onTagSelected, + super.onTap, + super.enableHeroAnimation = true, + }); @override Widget build(BuildContext context) { @@ -50,7 +37,7 @@ class DocumentListItem extends StatelessWidget { Row( children: [ AbsorbPointer( - absorbing: isAtLeastOneSelected, + absorbing: isSelectionActive, child: CorrespondentWidget( isClickable: isLabelClickable, correspondentId: document.correspondent, @@ -69,7 +56,7 @@ class DocumentListItem extends StatelessWidget { subtitle: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: AbsorbPointer( - absorbing: isAtLeastOneSelected, + absorbing: isSelectionActive, child: TagsWidget( isClickable: isLabelClickable, tagIds: document.tags, @@ -95,7 +82,7 @@ class DocumentListItem extends StatelessWidget { } void _onTap() { - if (isAtLeastOneSelected || isSelected) { + if (isSelectionActive || isSelected) { onSelected?.call(document); } else { onTap?.call(document); diff --git a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart deleted file mode 100644 index 4e750db..0000000 --- a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; -import 'package:paperless_mobile/features/settings/model/view_type.dart'; - -class AdaptiveDocumentsView extends StatelessWidget { - final DocumentsState state; - final ViewType viewType; - final Widget? beforeItems; - final void Function(DocumentModel) onTap; - final void Function(DocumentModel) onSelected; - final ScrollController scrollController; - final bool hasInternetConnection; - final bool isLabelClickable; - final void Function(int id)? onTagSelected; - final void Function(int? id)? onCorrespondentSelected; - final void Function(int? id)? onDocumentTypeSelected; - final void Function(int? id)? onStoragePathSelected; - final Widget pageLoadingWidget; - - const AdaptiveDocumentsView({ - super.key, - required this.onTap, - required this.scrollController, - required this.state, - required this.onSelected, - required this.hasInternetConnection, - this.isLabelClickable = true, - this.onTagSelected, - this.onCorrespondentSelected, - this.onDocumentTypeSelected, - this.onStoragePathSelected, - required this.pageLoadingWidget, - this.beforeItems, - required this.viewType, - }); - - @override - Widget build(BuildContext context) { - return CustomScrollView( - controller: scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverToBoxAdapter(child: beforeItems), - if (viewType == ViewType.list) _buildListView() else _buildGridView(), - if (state.hasLoaded && state.isLoading) - SliverToBoxAdapter(child: pageLoadingWidget), - ], - ); - } - - SliverList _buildListView() { - return SliverList( - delegate: SliverChildBuilderDelegate( - childCount: state.documents.length, - (context, index) { - final document = state.documents.elementAt(index); - return LabelRepositoriesProvider( - child: DocumentListItem( - isLabelClickable: isLabelClickable, - document: document, - onTap: onTap, - isSelected: state.selectedIds.contains(document.id), - onSelected: onSelected, - isAtLeastOneSelected: state.selection.isNotEmpty, - onTagSelected: onTagSelected, - onCorrespondentSelected: onCorrespondentSelected, - onDocumentTypeSelected: onDocumentTypeSelected, - onStoragePathSelected: onStoragePathSelected, - ), - ); - }, - ), - ); - } - - Widget _buildGridView() { - return SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - childAspectRatio: 1 / 2, - ), - itemCount: state.documents.length, - itemBuilder: (context, index) { - if (state.hasLoaded && - state.isLoading && - index == state.documents.length) { - return Center(child: pageLoadingWidget); - } - final document = state.documents.elementAt(index); - return DocumentGridItem( - document: document, - onTap: onTap, - isSelected: state.selectedIds.contains(document.id), - onSelected: onSelected, - isAtLeastOneSelected: state.selection.isNotEmpty, - isTagSelectedPredicate: (int tagId) { - return state.filter.tags is IdsTagsQuery - ? (state.filter.tags as IdsTagsQuery) - .includedIds - .contains(tagId) - : false; - }, - onTagSelected: onTagSelected, - ); - }, - ); - } -} diff --git a/lib/features/documents/view/widgets/search/document_filter_form.dart b/lib/features/documents/view/widgets/search/document_filter_form.dart new file mode 100644 index 0000000..f0157ac --- /dev/null +++ b/lib/features/documents/view/widgets/search/document_filter_form.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; +import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; +import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +import 'text_query_form_field.dart'; + +class DocumentFilterForm extends StatefulWidget { + static const fkCorrespondent = DocumentModel.correspondentKey; + static const fkDocumentType = DocumentModel.documentTypeKey; + static const fkStoragePath = DocumentModel.storagePathKey; + static const fkQuery = "query"; + static const fkCreatedAt = DocumentModel.createdKey; + static const fkAddedAt = DocumentModel.addedKey; + + static DocumentFilter assembleFilter( + GlobalKey formKey, DocumentFilter initialFilter) { + formKey.currentState?.save(); + final v = formKey.currentState!.value; + return DocumentFilter( + correspondent: + v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ?? + DocumentFilter.initial.correspondent, + documentType: v[DocumentFilterForm.fkDocumentType] as IdQueryParameter? ?? + DocumentFilter.initial.documentType, + storagePath: v[DocumentFilterForm.fkStoragePath] as IdQueryParameter? ?? + DocumentFilter.initial.storagePath, + tags: + v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags, + query: v[DocumentFilterForm.fkQuery] as TextQuery? ?? + DocumentFilter.initial.query, + created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery), + added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery), + asnQuery: initialFilter.asnQuery, + page: 1, + pageSize: initialFilter.pageSize, + sortField: initialFilter.sortField, + sortOrder: initialFilter.sortOrder, + ); + } + + final Widget? header; + final GlobalKey formKey; + final DocumentFilter initialFilter; + final ScrollController? scrollController; + final EdgeInsets padding; + const DocumentFilterForm({ + super.key, + this.header, + required this.formKey, + required this.initialFilter, + this.scrollController, + this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + }); + + @override + State createState() => _DocumentFilterFormState(); +} + +class _DocumentFilterFormState extends State { + late bool _allowOnlyExtendedQuery; + + @override + void initState() { + super.initState(); + _allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery; + } + + @override + Widget build(BuildContext context) { + return FormBuilder( + key: widget.formKey, + child: CustomScrollView( + controller: widget.scrollController, + slivers: [ + if (widget.header != null) widget.header!, + ..._buildFormFieldList(), + SliverToBoxAdapter( + child: SizedBox( + height: 32, + ), + ), + ], + ), + ); + } + + List _buildFormFieldList() { + return [ + _buildQueryFormField(), + Align( + alignment: Alignment.centerLeft, + child: Text( + S.of(context).documentFilterAdvancedLabel, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + FormBuilderExtendedDateRangePicker( + name: DocumentFilterForm.fkCreatedAt, + initialValue: widget.initialFilter.created, + labelText: S.of(context).documentCreatedPropertyLabel, + onChanged: (_) { + _checkQueryConstraints(); + }, + ), + FormBuilderExtendedDateRangePicker( + name: DocumentFilterForm.fkAddedAt, + initialValue: widget.initialFilter.added, + labelText: S.of(context).documentAddedPropertyLabel, + onChanged: (_) { + _checkQueryConstraints(); + }, + ), + _buildCorrespondentFormField(), + _buildDocumentTypeFormField(), + _buildStoragePathFormField(), + _buildTagsFormField(), + ] + .map((w) => SliverPadding( + padding: widget.padding, + sliver: SliverToBoxAdapter(child: w), + )) + .toList(); + } + + void _checkQueryConstraints() { + final filter = + DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter); + if (filter.forceExtendedQuery) { + setState(() => _allowOnlyExtendedQuery = true); + final queryField = + widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery]; + queryField?.didChange( + (queryField.value as TextQuery?) + ?.copyWith(queryType: QueryType.extended), + ); + } else { + setState(() => _allowOnlyExtendedQuery = false); + } + } + + Widget _buildDocumentTypeFormField() { + return BlocBuilder, LabelState>( + builder: (context, state) { + return LabelFormField( + formBuilderState: widget.formKey.currentState, + name: DocumentFilterForm.fkDocumentType, + labelOptions: state.labels, + textFieldLabel: S.of(context).documentDocumentTypePropertyLabel, + initialValue: widget.initialFilter.documentType, + prefixIcon: const Icon(Icons.description_outlined), + ); + }, + ); + } + + Widget _buildCorrespondentFormField() { + return BlocBuilder, LabelState>( + builder: (context, state) { + return LabelFormField( + formBuilderState: widget.formKey.currentState, + name: DocumentFilterForm.fkCorrespondent, + labelOptions: state.labels, + textFieldLabel: S.of(context).documentCorrespondentPropertyLabel, + initialValue: widget.initialFilter.correspondent, + prefixIcon: const Icon(Icons.person_outline), + ); + }, + ); + } + + Widget _buildStoragePathFormField() { + return BlocBuilder, LabelState>( + builder: (context, state) { + return LabelFormField( + formBuilderState: widget.formKey.currentState, + name: DocumentFilterForm.fkStoragePath, + labelOptions: state.labels, + textFieldLabel: S.of(context).documentStoragePathPropertyLabel, + initialValue: widget.initialFilter.storagePath, + prefixIcon: const Icon(Icons.folder_outlined), + ); + }, + ); + } + + Widget _buildQueryFormField() { + return TextQueryFormField( + name: DocumentFilterForm.fkQuery, + onlyExtendedQueryAllowed: _allowOnlyExtendedQuery, + initialValue: widget.initialFilter.query, + ); + } + + BlocBuilder, LabelState> _buildTagsFormField() { + return BlocBuilder, LabelState>( + builder: (context, state) { + return TagFormField( + name: DocumentModel.tagsKey, + initialValue: widget.initialFilter.tags, + allowCreation: false, + selectableOptions: state.labels, + ); + }, + ); + } +} diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 028b65b..8cc5e5f 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -7,6 +7,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/text_query_form_field.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; @@ -32,22 +33,14 @@ class DocumentFilterPanel extends StatefulWidget { } class _DocumentFilterPanelState extends State { - static const fkCorrespondent = DocumentModel.correspondentKey; - static const fkDocumentType = DocumentModel.documentTypeKey; - static const fkStoragePath = DocumentModel.storagePathKey; - static const fkQuery = "query"; - static const fkCreatedAt = DocumentModel.createdKey; - static const fkAddedAt = DocumentModel.addedKey; - final _formKey = GlobalKey(); - late bool _allowOnlyExtendedQuery; double _heightAnimationValue = 0; @override void initState() { super.initState(); - _allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery; + widget.draggableSheetController.addListener(animateTitleByDrag); } @@ -106,100 +99,59 @@ class _DocumentFilterPanelState extends State { ), ), resizeToAvoidBottomInset: true, - body: FormBuilder( - key: _formKey, - child: _buildFormList(context), + body: DocumentFilterForm( + formKey: _formKey, + scrollController: widget.scrollController, + initialFilter: widget.initialFilter, + header: _buildPanelHeader(), ), ), ); } - Widget _buildFormList(BuildContext context) { - return CustomScrollView( - controller: widget.scrollController, - slivers: [ - SliverAppBar( - pinned: true, - automaticallyImplyLeading: false, - toolbarHeight: kToolbarHeight + 22, - title: SizedBox( - width: MediaQuery.of(context).size.width, - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Opacity( - opacity: 1 - _heightAnimationValue, - child: Padding( - padding: EdgeInsets.only(bottom: 11), - child: _buildDragHandle(), - ), - ), - Align( - alignment: Alignment.centerLeft, - child: Stack( - alignment: Alignment.centerLeft, - children: [ - Opacity( - opacity: max(0, (_heightAnimationValue - 0.5) * 2), - child: GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: const Icon(Icons.expand_more_rounded), - ), - ), - Padding( - padding: - EdgeInsets.only(left: _heightAnimationValue * 48), - child: Text(S.of(context).documentFilterTitle), - ), - ], - ), - ), - ], + Widget _buildPanelHeader() { + return SliverAppBar( + pinned: true, + automaticallyImplyLeading: false, + toolbarHeight: kToolbarHeight + 22, + title: SizedBox( + width: MediaQuery.of(context).size.width, + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Opacity( + opacity: 1 - _heightAnimationValue, + child: Padding( + padding: const EdgeInsets.only(bottom: 11), + child: _buildDragHandle(), + ), ), - ), + Align( + alignment: Alignment.centerLeft, + child: Stack( + alignment: Alignment.centerLeft, + children: [ + Opacity( + opacity: max(0, (_heightAnimationValue - 0.5) * 2), + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: const Icon(Icons.expand_more_rounded), + ), + ), + Padding( + padding: EdgeInsets.only(left: _heightAnimationValue * 48), + child: Text(S.of(context).documentFilterTitle), + ), + ], + ), + ), + ], ), - ..._buildFormFieldList(), - ], + ), ); } - List _buildFormFieldList() { - return [ - _buildQueryFormField().paddedSymmetrically(vertical: 8, horizontal: 16), - Align( - alignment: Alignment.centerLeft, - child: Text( - S.of(context).documentFilterAdvancedLabel, - style: Theme.of(context).textTheme.bodySmall, - ), - ).paddedSymmetrically(vertical: 8, horizontal: 16), - FormBuilderExtendedDateRangePicker( - name: fkCreatedAt, - initialValue: widget.initialFilter.created, - labelText: S.of(context).documentCreatedPropertyLabel, - onChanged: (_) { - _checkQueryConstraints(); - }, - ).paddedSymmetrically(vertical: 8, horizontal: 16), - FormBuilderExtendedDateRangePicker( - name: fkAddedAt, - initialValue: widget.initialFilter.added, - labelText: S.of(context).documentAddedPropertyLabel, - onChanged: (_) { - _checkQueryConstraints(); - }, - ).paddedSymmetrically(vertical: 8, horizontal: 16), - _buildCorrespondentFormField() - .paddedSymmetrically(vertical: 8, horizontal: 16), - _buildDocumentTypeFormField() - .paddedSymmetrically(vertical: 8, horizontal: 16), - _buildStoragePathFormField() - .paddedSymmetrically(vertical: 8, horizontal: 16), - _buildTagsFormField().padded(16), - ].map((w) => SliverToBoxAdapter(child: w)).toList(); - } - Container _buildDragHandle() { return Container( // According to m3 spec https://m3.material.io/components/bottom-sheets/specs @@ -212,19 +164,6 @@ class _DocumentFilterPanelState extends State { ); } - BlocBuilder, LabelState> _buildTagsFormField() { - return BlocBuilder, LabelState>( - builder: (context, state) { - return TagFormField( - name: DocumentModel.tagsKey, - initialValue: widget.initialFilter.tags, - allowCreation: false, - selectableOptions: state.labels, - ); - }, - ); - } - void _resetFilter() async { FocusScope.of(context).unfocus(); Navigator.pop( @@ -233,102 +172,13 @@ class _DocumentFilterPanelState extends State { ); } - Widget _buildDocumentTypeFormField() { - return BlocBuilder, LabelState>( - builder: (context, state) { - return LabelFormField( - formBuilderState: _formKey.currentState, - name: fkDocumentType, - labelOptions: state.labels, - textFieldLabel: S.of(context).documentDocumentTypePropertyLabel, - initialValue: widget.initialFilter.documentType, - prefixIcon: const Icon(Icons.description_outlined), - ); - }, - ); - } - - Widget _buildCorrespondentFormField() { - return BlocBuilder, LabelState>( - builder: (context, state) { - return LabelFormField( - formBuilderState: _formKey.currentState, - name: fkCorrespondent, - labelOptions: state.labels, - textFieldLabel: S.of(context).documentCorrespondentPropertyLabel, - initialValue: widget.initialFilter.correspondent, - prefixIcon: const Icon(Icons.person_outline), - ); - }, - ); - } - - Widget _buildStoragePathFormField() { - return BlocBuilder, LabelState>( - builder: (context, state) { - return LabelFormField( - formBuilderState: _formKey.currentState, - name: fkStoragePath, - labelOptions: state.labels, - textFieldLabel: S.of(context).documentStoragePathPropertyLabel, - initialValue: widget.initialFilter.storagePath, - prefixIcon: const Icon(Icons.folder_outlined), - ); - }, - ); - } - - Widget _buildQueryFormField() { - return TextQueryFormField( - name: fkQuery, - onlyExtendedQueryAllowed: _allowOnlyExtendedQuery, - initialValue: widget.initialFilter.query, - ); - } - void _onApplyFilter() async { _formKey.currentState?.save(); if (_formKey.currentState?.validate() ?? false) { - DocumentFilter newFilter = _assembleFilter(); + DocumentFilter newFilter = + DocumentFilterForm.assembleFilter(_formKey, widget.initialFilter); FocusScope.of(context).unfocus(); Navigator.pop(context, DocumentFilterIntent(filter: newFilter)); } } - - DocumentFilter _assembleFilter() { - _formKey.currentState?.save(); - final v = _formKey.currentState!.value; - return DocumentFilter( - correspondent: v[fkCorrespondent] as IdQueryParameter? ?? - DocumentFilter.initial.correspondent, - documentType: v[fkDocumentType] as IdQueryParameter? ?? - DocumentFilter.initial.documentType, - storagePath: v[fkStoragePath] as IdQueryParameter? ?? - DocumentFilter.initial.storagePath, - tags: - v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags, - query: v[fkQuery] as TextQuery? ?? DocumentFilter.initial.query, - created: (v[fkCreatedAt] as DateRangeQuery), - added: (v[fkAddedAt] as DateRangeQuery), - asnQuery: widget.initialFilter.asnQuery, - page: 1, - pageSize: widget.initialFilter.pageSize, - sortField: widget.initialFilter.sortField, - sortOrder: widget.initialFilter.sortOrder, - ); - } - - void _checkQueryConstraints() { - final filter = _assembleFilter(); - if (filter.forceExtendedQuery) { - setState(() => _allowOnlyExtendedQuery = true); - final queryField = _formKey.currentState?.fields[fkQuery]; - queryField?.didChange( - (queryField.value as TextQuery?) - ?.copyWith(queryType: QueryType.extended), - ); - } else { - setState(() => _allowOnlyExtendedQuery = false); - } - } } diff --git a/lib/features/documents/view/widgets/view_actions.dart b/lib/features/documents/view/widgets/view_actions.dart new file mode 100644 index 0000000..a69feb0 --- /dev/null +++ b/lib/features/documents/view/widgets/view_actions.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; + +class ViewActions extends StatelessWidget { + const ViewActions({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SortDocumentsButton(), + BlocBuilder( + builder: (context, settings) { + final cubit = context.read(); + switch (settings.preferredViewType) { + case ViewType.grid: + return IconButton( + icon: const Icon(Icons.list), + onPressed: () => + cubit.setViewType(settings.preferredViewType.toggle()), + ); + case ViewType.list: + return IconButton( + icon: const Icon(Icons.grid_view_rounded), + onPressed: () => + cubit.setViewType(settings.preferredViewType.toggle()), + ); + } + }, + ) + ], + ); + } +} diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 9028c2d..d70f5f2 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.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/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; diff --git a/lib/features/linked_documents/view/pages/linked_documents_page.dart b/lib/features/linked_documents/view/pages/linked_documents_page.dart index b741104..b0956c2 100644 --- a/lib/features/linked_documents/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents/view/pages/linked_documents_page.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -48,14 +50,17 @@ class _LinkedDocumentsPageState extends State { ), body: BlocBuilder( builder: (context, state) { - if (!state.hasLoaded) { - return const DocumentsListLoadingWidget(); - } - return ListView.builder( - itemCount: state.documents.length, - itemBuilder: (context, index) => DocumentListItem( - document: state.documents[index], - ), + return BlocBuilder( + builder: (context, connectivity) { + return DefaultAdaptiveDocumentsView( + scrollController: _scrollController, + documents: state.documents, + hasInternetConnection: connectivity.isConnected, + isLabelClickable: false, + isLoading: state.isLoading, + hasLoaded: state.hasLoaded, + ); + }, ); }, ), diff --git a/lib/features/saved_view/cubit/saved_view_details_cubit.dart b/lib/features/saved_view/cubit/saved_view_details_cubit.dart new file mode 100644 index 0000000..9b2dfd7 --- /dev/null +++ b/lib/features/saved_view/cubit/saved_view_details_cubit.dart @@ -0,0 +1,20 @@ +import 'package:bloc/bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; + +part 'saved_view_details_state.dart'; + +class SavedViewDetailsCubit extends Cubit + with PagedDocumentsMixin { + @override + final PaperlessDocumentsApi api; + + final SavedView savedView; + SavedViewDetailsCubit( + this.api, { + required this.savedView, + }) : super(const SavedViewDetailsState()) { + updateFilter(filter: savedView.toDocumentFilter()); + } +} diff --git a/lib/features/saved_view/cubit/saved_view_details_state.dart b/lib/features/saved_view/cubit/saved_view_details_state.dart new file mode 100644 index 0000000..653d9e4 --- /dev/null +++ b/lib/features/saved_view/cubit/saved_view_details_state.dart @@ -0,0 +1,47 @@ +part of 'saved_view_details_cubit.dart'; + +class SavedViewDetailsState extends PagedDocumentsState { + const SavedViewDetailsState({ + super.filter, + super.hasLoaded, + super.isLoading, + super.value, + }); + + @override + List get props => [ + filter, + hasLoaded, + isLoading, + value, + ]; + + @override + SavedViewDetailsState copyWithPaged({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return copyWith( + hasLoaded: hasLoaded, + isLoading: isLoading, + value: value, + filter: filter, + ); + } + + SavedViewDetailsState copyWith({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return SavedViewDetailsState( + hasLoaded: hasLoaded ?? this.hasLoaded, + isLoading: isLoading ?? this.isLoading, + value: value ?? this.value, + filter: filter ?? this.filter, + ); + } +} diff --git a/lib/features/saved_view/view/add_saved_view_page.dart b/lib/features/saved_view/view/add_saved_view_page.dart index 761668e..1e1d8ce 100644 --- a/lib/features/saved_view/view/add_saved_view_page.dart +++ b/lib/features/saved_view/view/add_saved_view_page.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; @@ -17,21 +20,13 @@ class _AddSavedViewPageState extends State { static const fkShowOnDashboard = 'show_on_dashboard'; static const fkShowInSidebar = 'show_in_sidebar'; - final GlobalKey _formKey = GlobalKey(); + final _savedViewFormKey = GlobalKey(); + final _filterFormKey = GlobalKey(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(S.of(context).savedViewCreateNewLabel), - actions: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Tooltip( - child: const Icon(Icons.info_outline), - message: S.of(context).savedViewCreateTooltipText, - ), - ), - ], ), floatingActionButton: FloatingActionButton.extended( icon: const Icon(Icons.add), @@ -40,44 +35,102 @@ class _AddSavedViewPageState extends State { ), body: Padding( padding: const EdgeInsets.all(8.0), - child: FormBuilder( - key: _formKey, - child: ListView( - children: [ - FormBuilderTextField( - name: fkName, - validator: FormBuilderValidators.required(), - decoration: InputDecoration( - label: Text(S.of(context).savedViewNameLabel), - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + FormBuilder( + key: _savedViewFormKey, + child: Column( + children: [ + FormBuilderTextField( + name: _AddSavedViewPageState.fkName, + validator: FormBuilderValidators.required(), + decoration: InputDecoration( + label: Text(S.of(context).savedViewNameLabel), + ), + ), + FormBuilderCheckbox( + name: _AddSavedViewPageState.fkShowOnDashboard, + initialValue: false, + title: Text(S.of(context).savedViewShowOnDashboardLabel), + ), + FormBuilderCheckbox( + name: _AddSavedViewPageState.fkShowInSidebar, + initialValue: false, + title: Text(S.of(context).savedViewShowInSidebarLabel), + ), + ], ), - FormBuilderCheckbox( - name: fkShowOnDashboard, - initialValue: false, - title: Text(S.of(context).savedViewShowOnDashboardLabel), + ), + Divider(), + Text( + "Review filter", + style: Theme.of(context).textTheme.bodyLarge, + ).padded(), + Flexible( + child: DocumentFilterForm( + padding: const EdgeInsets.symmetric(vertical: 8), + formKey: _filterFormKey, + initialFilter: widget.currentFilter, ), - FormBuilderCheckbox( - name: fkShowInSidebar, - initialValue: false, - title: Text(S.of(context).savedViewShowInSidebarLabel), - ), - ], - ), + ), + ], ), ), ); } + Padding _buildOld(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + FormBuilder( + key: _savedViewFormKey, + child: Expanded( + child: ListView( + children: [ + FormBuilderTextField( + name: fkName, + validator: FormBuilderValidators.required(), + decoration: InputDecoration( + label: Text(S.of(context).savedViewNameLabel), + ), + ), + FormBuilderCheckbox( + name: fkShowOnDashboard, + initialValue: false, + title: Text(S.of(context).savedViewShowOnDashboardLabel), + ), + FormBuilderCheckbox( + name: fkShowInSidebar, + initialValue: false, + title: Text(S.of(context).savedViewShowInSidebarLabel), + ), + ], + ), + ), + ), + ], + ), + ); + } + void _onCreate(BuildContext context) { - if (_formKey.currentState?.saveAndValidate() ?? false) { + if (_savedViewFormKey.currentState?.saveAndValidate() ?? false) { Navigator.pop( context, SavedView.fromDocumentFilter( - widget.currentFilter, - name: _formKey.currentState?.value[fkName] as String, + DocumentFilterForm.assembleFilter( + _filterFormKey, + widget.currentFilter, + ), + name: _savedViewFormKey.currentState?.value[fkName] as String, showOnDashboard: - _formKey.currentState?.value[fkShowOnDashboard] as bool, - showInSidebar: _formKey.currentState?.value[fkShowInSidebar] as bool, + _savedViewFormKey.currentState?.value[fkShowOnDashboard] as bool, + showInSidebar: + _savedViewFormKey.currentState?.value[fkShowInSidebar] as bool, ), ); } diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart new file mode 100644 index 0000000..04f09ef --- /dev/null +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; +import 'package:paperless_mobile/features/saved_view/view/saved_view_page.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class SavedViewList extends StatelessWidget { + const SavedViewList({super.key}); + + @override + Widget build(BuildContext context) { + final savedViewCubit = context.read(); + return BlocBuilder( + builder: (context, state) { + if (state.value.isEmpty) { + return Text( + S.of(context).savedViewsEmptyStateText, + textAlign: TextAlign.center, + ).padded(); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final view = state.value.values.elementAt(index); + return ListTile( + title: Text(view.name), + subtitle: Text( + "${view.filterRules.length} filter(s) set"), //TODO: INTL w/ placeholder + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SavedViewDetailsCubit( + context.read(), + savedView: view, + ), + ), + BlocProvider.value(value: savedViewCubit), + ], + child: SavedViewPage( + onDelete: savedViewCubit.remove, + ), + ), + ), + ); + }, + ); + }, + childCount: state.value.length, + ), + ); + }, + ); + } +} diff --git a/lib/features/saved_view/view/saved_view_page.dart b/lib/features/saved_view/view/saved_view_page.dart new file mode 100644 index 0000000..84f7b20 --- /dev/null +++ b/lib/features/saved_view/view/saved_view_page.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/view_actions.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; + +class SavedViewPage extends StatefulWidget { + final Future Function(SavedView savedView) onDelete; + const SavedViewPage({ + super.key, + required this.onDelete, + }); + + @override + State createState() => _SavedViewPageState(); +} + +class _SavedViewPageState extends State { + final _scrollController = ScrollController(); + ViewType _viewType = ViewType.list; + SavedView get _savedView => context.read().savedView; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_listenForLoadNewData); + } + + void _listenForLoadNewData() async { + final currState = context.read().state; + if (_scrollController.offset >= + _scrollController.position.maxScrollExtent * 0.7 && + !currState.isLoading && + !currState.isLastPageLoaded) { + try { + await context.read().loadMore(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: BlocBuilder( + builder: (context, state) { + return Text(_savedView.name); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + final shouldDelete = await showDialog( + context: context, + builder: (context) => + ConfirmDeleteSavedViewDialog(view: _savedView), + ) ?? + false; + if (shouldDelete) { + await widget.onDelete(_savedView); + Navigator.pop(context); + } + }, + ), + IconButton( + icon: Icon( + _viewType == ViewType.list ? Icons.grid_view_rounded : Icons.list, + ), + onPressed: () => setState(() => _viewType = _viewType.toggle()), + ), + ], + ), + body: BlocBuilder( + builder: (context, state) { + if (state.hasLoaded && state.documents.isEmpty) { + return DocumentsEmptyState(state: state); + } + return BlocBuilder( + builder: (context, connectivity) { + return CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAdaptiveDocumentsView( + documents: state.documents, + hasInternetConnection: connectivity.isConnected, + isLabelClickable: false, + isLoading: state.isLoading, + hasLoaded: state.hasLoaded, + onTap: _onOpenDocumentDetails, + viewType: _viewType, + ), + ], + ); + }, + ); + }, + ), + ); + } + + void _onOpenDocumentDetails(DocumentModel document) async { + final updatedDocument = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => BlocProvider( + create: (context) => DocumentDetailsCubit( + context.read(), + document, + ), + child: const LabelRepositoriesProvider( + child: DocumentDetailsPage(), + ), + ), + ), + ); + if (updatedDocument != document) { + // Reload in case document was edited and might not fulfill filter criteria of saved view anymore + context.read().reload(); + } + } +} diff --git a/lib/features/saved_view/view/saved_view_selection_widget.dart b/lib/features/saved_view/view/saved_view_selection_widget.dart index bbc21b8..43d71b3 100644 --- a/lib/features/saved_view/view/saved_view_selection_widget.dart +++ b/lib/features/saved_view/view/saved_view_selection_widget.dart @@ -1,218 +1,218 @@ -import 'dart:math'; +// import 'dart:math'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; -import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; -import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; -import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/constants.dart'; -import 'package:shimmer/shimmer.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:paperless_api/paperless_api.dart'; +// import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +// import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +// import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; +// import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +// import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; +// import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; +// import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; +// import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; +// import 'package:paperless_mobile/generated/l10n.dart'; +// import 'package:paperless_mobile/helpers/message_helpers.dart'; +// import 'package:paperless_mobile/constants.dart'; +// import 'package:shimmer/shimmer.dart'; -class SavedViewSelectionWidget extends StatelessWidget { - final DocumentFilter currentFilter; - const SavedViewSelectionWidget({ - Key? key, - required this.height, - required this.enabled, - required this.currentFilter, - }) : super(key: key); +// class SavedViewSelectionWidget extends StatelessWidget { +// final DocumentFilter currentFilter; +// const SavedViewSelectionWidget({ +// Key? key, +// required this.height, +// required this.enabled, +// required this.currentFilter, +// }) : super(key: key); - final double height; - final bool enabled; +// final double height; +// final bool enabled; - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, connectivityState) { - final hasInternetConnection = connectivityState.isConnected; - return SizedBox( - height: height, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BlocBuilder( - builder: (context, state) { - if (!state.hasLoaded) { - return _buildLoadingWidget(context); - } - if (state.value.isEmpty) { - return Text(S.of(context).savedViewsEmptyStateText); - } - return SizedBox( - height: 38, - child: ListView.separated( - itemCount: state.value.length, - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - final view = state.value.values.elementAt(index); - return GestureDetector( - onLongPress: hasInternetConnection - ? () => _onDelete(context, view) - : null, - child: BlocBuilder( - builder: (context, docState) { - final view = state.value.values.toList()[index]; - return FilterChip( - label: Text( - view.name, - ), - selected: - view.id == docState.selectedSavedViewId, - onSelected: enabled && hasInternetConnection - ? (isSelected) => - _onSelected(isSelected, context, view) - : null, - ); - }, - ), - ); - }, - separatorBuilder: (context, index) => const SizedBox( - width: 4.0, - ), - ), - ); - }, - ), - BlocBuilder( - builder: (context, state) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).savedViewsLabel, - style: Theme.of(context).textTheme.titleSmall, - ), - BlocBuilder( - buildWhen: (previous, current) => - previous.filter != current.filter, - builder: (context, docState) { - return TextButton.icon( - icon: const Icon(Icons.add), - onPressed: (enabled && - state.hasLoaded && - hasInternetConnection) - ? () => - _onCreatePressed(context, docState.filter) - : null, - label: Text(S.of(context).savedViewCreateNewLabel), - ); - }, - ), - ], - ); - }, - ), - ], - ).padded(), - ); - }, - ); - } +// @override +// Widget build(BuildContext context) { +// return BlocBuilder( +// builder: (context, connectivityState) { +// final hasInternetConnection = connectivityState.isConnected; +// return SizedBox( +// height: height, +// child: Column( +// mainAxisAlignment: MainAxisAlignment.start, +// crossAxisAlignment: CrossAxisAlignment.start, +// mainAxisSize: MainAxisSize.min, +// children: [ +// BlocBuilder( +// builder: (context, state) { +// if (!state.hasLoaded) { +// return _buildLoadingWidget(context); +// } +// if (state.value.isEmpty) { +// return Text(S.of(context).savedViewsEmptyStateText); +// } +// return SizedBox( +// height: 38, +// child: ListView.separated( +// itemCount: state.value.length, +// scrollDirection: Axis.horizontal, +// itemBuilder: (context, index) { +// final view = state.value.values.elementAt(index); +// return GestureDetector( +// onLongPress: hasInternetConnection +// ? () => _onDelete(context, view) +// : null, +// child: BlocBuilder( +// builder: (context, docState) { +// final view = state.value.values.toList()[index]; +// return FilterChip( +// label: Text( +// view.name, +// ), +// selected: +// view.id == docState.selectedSavedViewId, +// onSelected: enabled && hasInternetConnection +// ? (isSelected) => +// _onSelected(isSelected, context, view) +// : null, +// ); +// }, +// ), +// ); +// }, +// separatorBuilder: (context, index) => const SizedBox( +// width: 4.0, +// ), +// ), +// ); +// }, +// ), +// BlocBuilder( +// builder: (context, state) { +// return Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Text( +// S.of(context).savedViewsLabel, +// style: Theme.of(context).textTheme.titleSmall, +// ), +// BlocBuilder( +// buildWhen: (previous, current) => +// previous.filter != current.filter, +// builder: (context, docState) { +// return TextButton.icon( +// icon: const Icon(Icons.add), +// onPressed: (enabled && +// state.hasLoaded && +// hasInternetConnection) +// ? () => +// _onCreatePressed(context, docState.filter) +// : null, +// label: Text(S.of(context).savedViewCreateNewLabel), +// ); +// }, +// ), +// ], +// ); +// }, +// ), +// ], +// ).padded(), +// ); +// }, +// ); +// } - Widget _buildLoadingWidget(BuildContext context) { - return SizedBox( - height: 38, - width: MediaQuery.of(context).size.width, - child: Shimmer.fromColors( - baseColor: Theme.of(context).brightness == Brightness.light - ? Colors.grey[300]! - : Colors.grey[900]!, - highlightColor: Theme.of(context).brightness == Brightness.light - ? Colors.grey[100]! - : Colors.grey[600]!, - child: ListView( - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - children: [ - FilterChip( - label: const SizedBox(width: 32), - onSelected: (_) {}, - ), - const SizedBox(width: 4.0), - FilterChip( - label: const SizedBox(width: 64), - onSelected: (_) {}, - ), - const SizedBox(width: 4.0), - FilterChip( - label: const SizedBox(width: 100), - onSelected: (_) {}, - ), - const SizedBox(width: 4.0), - FilterChip( - label: const SizedBox(width: 32), - onSelected: (_) {}, - ), - const SizedBox(width: 4.0), - FilterChip( - label: const SizedBox(width: 48), - onSelected: (_) {}, - ), - ], - ), - ), - ); - } +// Widget _buildLoadingWidget(BuildContext context) { +// return SizedBox( +// height: 38, +// width: MediaQuery.of(context).size.width, +// child: Shimmer.fromColors( +// baseColor: Theme.of(context).brightness == Brightness.light +// ? Colors.grey[300]! +// : Colors.grey[900]!, +// highlightColor: Theme.of(context).brightness == Brightness.light +// ? Colors.grey[100]! +// : Colors.grey[600]!, +// child: ListView( +// scrollDirection: Axis.horizontal, +// physics: const NeverScrollableScrollPhysics(), +// children: [ +// FilterChip( +// label: const SizedBox(width: 32), +// onSelected: (_) {}, +// ), +// const SizedBox(width: 4.0), +// FilterChip( +// label: const SizedBox(width: 64), +// onSelected: (_) {}, +// ), +// const SizedBox(width: 4.0), +// FilterChip( +// label: const SizedBox(width: 100), +// onSelected: (_) {}, +// ), +// const SizedBox(width: 4.0), +// FilterChip( +// label: const SizedBox(width: 32), +// onSelected: (_) {}, +// ), +// const SizedBox(width: 4.0), +// FilterChip( +// label: const SizedBox(width: 48), +// onSelected: (_) {}, +// ), +// ], +// ), +// ), +// ); +// } - void _onCreatePressed(BuildContext context, DocumentFilter filter) async { - final newView = await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => AddSavedViewPage( - currentFilter: filter, - ), - ), - ); - if (newView != null) { - try { - await context.read().add(newView); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - } +// void _onCreatePressed(BuildContext context, DocumentFilter filter) async { +// final newView = await Navigator.of(context).push( +// MaterialPageRoute( +// builder: (context) => AddSavedViewPage( +// currentFilter: filter, +// ), +// ), +// ); +// if (newView != null) { +// try { +// await context.read().add(newView); +// } on PaperlessServerException catch (error, stackTrace) { +// showErrorMessage(context, error, stackTrace); +// } +// } +// } - void _onSelected( - bool selectionIntent, - BuildContext context, - SavedView view, - ) async { - if (selectionIntent) { - context.read().selectView(view.id!); - } else { - context.read().unselectView(); - context.read().resetFilter(); - } - } +// void _onSelected( +// bool selectionIntent, +// BuildContext context, +// SavedView view, +// ) async { +// if (selectionIntent) { +// context.read().selectView(view.id!); +// } else { +// context.read().unselectView(); +// context.read().resetFilter(); +// } +// } - void _onDelete(BuildContext context, SavedView view) async { - { - final delete = await showDialog( - context: context, - builder: (context) => ConfirmDeleteSavedViewDialog(view: view), - ) ?? - false; - if (delete) { - try { - context.read().remove(view); - if (context.read().state.selectedSavedViewId == - view.id) { - await context.read().resetFilter(); - } - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - } - } -} +// void _onDelete(BuildContext context, SavedView view) async { +// { +// final delete = await showDialog( +// context: context, +// builder: (context) => ConfirmDeleteSavedViewDialog(view: view), +// ) ?? +// false; +// if (delete) { +// try { +// context.read().remove(view); +// if (context.read().state.selectedSavedViewId == +// view.id) { +// await context.read().resetFilter(); +// } +// } on PaperlessServerException catch (error, stackTrace) { +// showErrorMessage(context, error, stackTrace); +// } +// } +// } +// } +// } diff --git a/lib/features/search/view/document_search_page.dart b/lib/features/search/view/document_search_page.dart index 2bedd3d..a7255a2 100644 --- a/lib/features/search/view/document_search_page.dart +++ b/lib/features/search/view/document_search_page.dart @@ -1,13 +1,10 @@ import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; -import 'package:paperless_mobile/features/search/cubit/document_search_state.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/search/cubit/document_search_cubit.dart'; +import 'package:paperless_mobile/features/search/cubit/document_search_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; Future showDocumentSearchPage(BuildContext context) { @@ -48,28 +45,33 @@ class _DocumentSearchPageState extends State { color: theme.colorScheme.onSurface, ), decoration: InputDecoration( + contentPadding: EdgeInsets.zero, hintStyle: theme.textTheme.bodyLarge?.apply( color: theme.colorScheme.onSurfaceVariant, ), - hintText: "Search documents", + hintText: "Search documents", //TODO: INTL border: InputBorder.none, ), controller: _queryController, onChanged: context.read().suggest, - onSubmitted: context.read().search, + textInputAction: TextInputAction.search, + onSubmitted: (query) { + FocusScope.of(context).unfocus(); + context.read().search(query); + }, ), actions: [ IconButton( color: theme.colorScheme.onSurfaceVariant, - icon: Icon(Icons.clear), + icon: const Icon(Icons.clear), onPressed: () { context.read().reset(); _queryController.clear(); }, - ) + ).padded(), ], bottom: PreferredSize( - preferredSize: Size.fromHeight(1), + preferredSize: const Size.fromHeight(1), child: Divider( color: theme.colorScheme.outline, ), @@ -103,7 +105,7 @@ class _DocumentSearchPageState extends State { delegate: SliverChildBuilderDelegate( (context, index) => ListTile( title: Text(historyMatches[index]), - leading: Icon(Icons.history), + leading: const Icon(Icons.history), onTap: () => _selectSuggestion(historyMatches[index]), ), childCount: historyMatches.length, @@ -120,7 +122,7 @@ class _DocumentSearchPageState extends State { delegate: SliverChildBuilderDelegate( (context, index) => ListTile( title: Text(suggestions[index]), - leading: Icon(Icons.search), + leading: const Icon(Icons.search), onTap: () => _selectSuggestion(suggestions[index]), ), childCount: suggestions.length, @@ -135,27 +137,21 @@ class _DocumentSearchPageState extends State { S.of(context).documentSearchResults, style: Theme.of(context).textTheme.labelSmall, ).padded(); - if (state.isLoading) { - return DocumentsListLoadingWidget( - beforeWidgets: [header], - ); - } return CustomScrollView( slivers: [ SliverToBoxAdapter(child: header), if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) - SliverToBoxAdapter( - child: Center(child: Text("No documents found.")), + const SliverToBoxAdapter( + child: Center(child: Text("No documents found.")), //TODO: INTL ) else - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => DocumentListItem( - document: state.documents[index], - ), - childCount: state.documents.length, - ), - ), + SliverAdaptiveDocumentsView( + documents: state.documents, + hasInternetConnection: true, + isLabelClickable: false, + isLoading: state.isLoading, + hasLoaded: state.hasLoaded, + ) ], ); } diff --git a/lib/features/search/view/documents_search_app_bar.dart b/lib/features/search/view/documents_search_app_bar.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/features/search/view/documents_search_app_bar.dart @@ -0,0 +1 @@ + diff --git a/lib/features/search_app_bar/view/search_app_bar.dart b/lib/features/search_app_bar/view/search_app_bar.dart new file mode 100644 index 0000000..08ddb62 --- /dev/null +++ b/lib/features/search_app_bar/view/search_app_bar.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; + +typedef OpenSearchCallback = void Function(BuildContext context); + +class SearchAppBar extends StatefulWidget with PreferredSizeWidget { + final PreferredSizeWidget? bottom; + final OpenSearchCallback onOpenSearch; + final Color? backgroundColor; + const SearchAppBar({ + super.key, + required this.onOpenSearch, + this.bottom, + this.backgroundColor, + }); + + @override + State createState() => _SearchAppBarState(); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _SearchAppBarState extends State { + @override + Widget build(BuildContext context) { + return SliverAppBar( + floating: true, + pinned: true, + snap: true, + backgroundColor: widget.backgroundColor, + title: SearchBar( + height: kToolbarHeight - 8, + supportingText: "Search documents", + onTap: () => widget.onOpenSearch(context), + leadingIcon: IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ), + trailingIcon: IconButton( + icon: const CircleAvatar( + child: Text("A"), + ), + onPressed: () {}, + ), + ).paddedOnly(top: 4, bottom: 4), + bottom: widget.bottom, + ); + } +} From 748cd21713c7dea0b6f545db445a996c4e91f6fa Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Wed, 1 Feb 2023 00:27:56 +0100 Subject: [PATCH 13/25] Added search bar on all pages --- lib/core/widgets/app_options_popup_menu.dart | 412 +++++++++--------- .../material/search/m3_search_bar.dart | 5 +- lib/core/widgets/paperless_logo.dart | 20 +- lib/features/app_drawer/view/app_drawer.dart | 117 +++++ .../bloc/document_details_cubit.dart | 7 +- .../view/pages/document_details_page.dart | 1 - .../cubit/document_search_cubit.dart | 2 +- .../cubit/document_search_state.dart | 7 +- .../cubit/document_search_state.g.dart | 0 .../view/document_search_page.dart | 21 +- .../view/documents_search_app_bar.dart | 0 .../documents/bloc/documents_cubit.dart | 5 +- .../documents/view/pages/documents_page.dart | 46 +- lib/features/home/view/home_page.dart | 40 +- lib/features/inbox/view/pages/inbox_page.dart | 198 ++++----- .../inbox/view/widgets/inbox_item.dart | 25 +- .../tags/view/widgets/tags_form_field.dart | 12 + .../labels/view/pages/labels_page.dart | 400 +++++++++++------ .../labels/view/widgets/label_tab_view.dart | 73 ++-- .../view/pages/linked_documents_page.dart | 14 + .../saved_view/view/saved_view_list.dart | 3 +- .../saved_view/view/saved_view_page.dart | 25 +- lib/features/scan/view/scanner_page.dart | 96 +++- .../search_app_bar/view/search_app_bar.dart | 17 +- .../view/dialogs/account_settings_dialog.dart | 103 +++++ lib/features/settings/view/settings_page.dart | 37 +- lib/l10n/intl_cs.arb | 3 +- lib/l10n/intl_de.arb | 3 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_tr.arb | 3 +- lib/main.dart | 5 + lib/routes/document_details_route.dart | 46 ++ .../lib/src/models/document_model.dart | 8 +- pubspec.lock | 16 + pubspec.yaml | 2 + 35 files changed, 1122 insertions(+), 653 deletions(-) rename lib/features/{search => document_search}/cubit/document_search_cubit.dart (94%) rename lib/features/{search => document_search}/cubit/document_search_state.dart (95%) rename lib/features/{search => document_search}/cubit/document_search_state.g.dart (100%) rename lib/features/{search => document_search}/view/document_search_page.dart (85%) rename lib/features/{search => document_search}/view/documents_search_app_bar.dart (100%) create mode 100644 lib/features/settings/view/dialogs/account_settings_dialog.dart create mode 100644 lib/routes/document_details_route.dart diff --git a/lib/core/widgets/app_options_popup_menu.dart b/lib/core/widgets/app_options_popup_menu.dart index ba97902..f02feb5 100644 --- a/lib/core/widgets/app_options_popup_menu.dart +++ b/lib/core/widgets/app_options_popup_menu.dart @@ -1,217 +1,217 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/constants.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; -import 'package:paperless_mobile/features/settings/model/view_type.dart'; -import 'package:paperless_mobile/features/settings/view/settings_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:url_launcher/link.dart'; -import 'package:url_launcher/url_launcher_string.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:paperless_mobile/constants.dart'; +// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +// import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; +// import 'package:paperless_mobile/features/settings/model/view_type.dart'; +// import 'package:paperless_mobile/features/settings/view/settings_page.dart'; +// import 'package:paperless_mobile/generated/l10n.dart'; +// import 'package:url_launcher/link.dart'; +// import 'package:url_launcher/url_launcher_string.dart'; -/// Declares selectable actions in menu. -enum AppPopupMenuEntries { - // Documents preview - documentsSelectListView, - documentsSelectGridView, - // Generic actions - openAboutThisAppDialog, - reportBug, - openSettings, - // Adds a divider - divider; -} +// /// Declares selectable actions in menu. +// enum AppPopupMenuEntries { +// // Documents preview +// documentsSelectListView, +// documentsSelectGridView, +// // Generic actions +// openAboutThisAppDialog, +// reportBug, +// openSettings, +// // Adds a divider +// divider; +// } -class AppOptionsPopupMenu extends StatelessWidget { - final List displayedActions; - const AppOptionsPopupMenu({ - super.key, - required this.displayedActions, - }); +// class AppOptionsPopupMenu extends StatelessWidget { +// final List displayedActions; +// const AppOptionsPopupMenu({ +// super.key, +// required this.displayedActions, +// }); - @override - Widget build(BuildContext context) { - return PopupMenuButton( - position: PopupMenuPosition.under, - icon: const Icon(Icons.more_vert), - onSelected: (action) { - switch (action) { - case AppPopupMenuEntries.documentsSelectListView: - context.read().setViewType(ViewType.list); - break; - case AppPopupMenuEntries.documentsSelectGridView: - context.read().setViewType(ViewType.grid); - break; - case AppPopupMenuEntries.openAboutThisAppDialog: - _showAboutDialog(context); - break; - case AppPopupMenuEntries.openSettings: - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: context.read(), - child: const SettingsPage(), - ), - ), - ); - break; - case AppPopupMenuEntries.reportBug: - launchUrlString( - 'https://github.com/astubenbord/paperless-mobile/issues/new', - ); - break; - default: - break; - } - }, - itemBuilder: _buildEntries, - ); - } +// @override +// Widget build(BuildContext context) { +// return PopupMenuButton( +// position: PopupMenuPosition.under, +// icon: const Icon(Icons.more_vert), +// onSelected: (action) { +// switch (action) { +// case AppPopupMenuEntries.documentsSelectListView: +// context.read().setViewType(ViewType.list); +// break; +// case AppPopupMenuEntries.documentsSelectGridView: +// context.read().setViewType(ViewType.grid); +// break; +// case AppPopupMenuEntries.openAboutThisAppDialog: +// _showAboutDialog(context); +// break; +// case AppPopupMenuEntries.openSettings: +// Navigator.of(context).push( +// MaterialPageRoute( +// builder: (context) => BlocProvider.value( +// value: context.read(), +// child: const SettingsPage(), +// ), +// ), +// ); +// break; +// case AppPopupMenuEntries.reportBug: +// launchUrlString( +// 'https://github.com/astubenbord/paperless-mobile/issues/new', +// ); +// break; +// default: +// break; +// } +// }, +// itemBuilder: _buildEntries, +// ); +// } - PopupMenuItem _buildReportBugTile(BuildContext context) { - return PopupMenuItem( - value: AppPopupMenuEntries.reportBug, - padding: EdgeInsets.zero, - child: ListTile( - leading: const Icon(Icons.bug_report), - title: Text(S.of(context).appDrawerReportBugLabel), - ), - ); - } +// PopupMenuItem _buildReportBugTile(BuildContext context) { +// return PopupMenuItem( +// value: AppPopupMenuEntries.reportBug, +// padding: EdgeInsets.zero, +// child: ListTile( +// leading: const Icon(Icons.bug_report), +// title: Text(S.of(context).appDrawerReportBugLabel), +// ), +// ); +// } - PopupMenuItem _buildSettingsTile(BuildContext context) { - return PopupMenuItem( - padding: EdgeInsets.zero, - value: AppPopupMenuEntries.openSettings, - child: ListTile( - leading: const Icon(Icons.settings_outlined), - title: Text(S.of(context).appDrawerSettingsLabel), - ), - ); - } +// PopupMenuItem _buildSettingsTile(BuildContext context) { +// return PopupMenuItem( +// padding: EdgeInsets.zero, +// value: AppPopupMenuEntries.openSettings, +// child: ListTile( +// leading: const Icon(Icons.settings_outlined), +// title: Text(S.of(context).appDrawerSettingsLabel), +// ), +// ); +// } - PopupMenuItem _buildAboutTile(BuildContext context) { - return PopupMenuItem( - padding: EdgeInsets.zero, - value: AppPopupMenuEntries.openAboutThisAppDialog, - child: ListTile( - leading: const Icon(Icons.info_outline), - title: Text(S.of(context).appDrawerAboutLabel), - ), - ); - } +// PopupMenuItem _buildAboutTile(BuildContext context) { +// return PopupMenuItem( +// padding: EdgeInsets.zero, +// value: AppPopupMenuEntries.openAboutThisAppDialog, +// child: ListTile( +// leading: const Icon(Icons.info_outline), +// title: Text(S.of(context).appDrawerAboutLabel), +// ), +// ); +// } - PopupMenuItem _buildListViewTile() { - return PopupMenuItem( - padding: EdgeInsets.zero, - child: BlocBuilder( - builder: (context, state) { - return ListTile( - leading: const Icon(Icons.list), - title: const Text("List"), - trailing: state.preferredViewType == ViewType.list - ? const Icon(Icons.check) - : null, - ); - }, - ), - value: AppPopupMenuEntries.documentsSelectListView, - ); - } +// PopupMenuItem _buildListViewTile() { +// return PopupMenuItem( +// padding: EdgeInsets.zero, +// child: BlocBuilder( +// builder: (context, state) { +// return ListTile( +// leading: const Icon(Icons.list), +// title: const Text("List"), +// trailing: state.preferredViewType == ViewType.list +// ? const Icon(Icons.check) +// : null, +// ); +// }, +// ), +// value: AppPopupMenuEntries.documentsSelectListView, +// ); +// } - PopupMenuItem _buildGridViewTile() { - return PopupMenuItem( - value: AppPopupMenuEntries.documentsSelectGridView, - padding: EdgeInsets.zero, - child: BlocBuilder( - builder: (context, state) { - return ListTile( - leading: const Icon(Icons.grid_view_rounded), - title: const Text("Grid"), - trailing: state.preferredViewType == ViewType.grid - ? const Icon(Icons.check) - : null, - ); - }, - ), - ); - } +// PopupMenuItem _buildGridViewTile() { +// return PopupMenuItem( +// value: AppPopupMenuEntries.documentsSelectGridView, +// padding: EdgeInsets.zero, +// child: BlocBuilder( +// builder: (context, state) { +// return ListTile( +// leading: const Icon(Icons.grid_view_rounded), +// title: const Text("Grid"), +// trailing: state.preferredViewType == ViewType.grid +// ? const Icon(Icons.check) +// : null, +// ); +// }, +// ), +// ); +// } - void _showAboutDialog(BuildContext context) { - showAboutDialog( - context: context, - applicationIcon: const ImageIcon( - AssetImage('assets/logos/paperless_logo_green.png'), - ), - applicationName: 'Paperless Mobile', - applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, - children: [ - Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), - Link( - uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), - builder: (context, followLink) => GestureDetector( - onTap: followLink, - child: Text( - 'https://github.com/astubenbord/paperless-mobile', - style: TextStyle(color: Theme.of(context).colorScheme.tertiary), - ), - ), - ), - const SizedBox(height: 16), - Text( - 'Credits', - style: Theme.of(context).textTheme.titleMedium, - ), - _buildOnboardingImageCredits(), - ], - ); - } +// void _showAboutDialog(BuildContext context) { +// showAboutDialog( +// context: context, +// applicationIcon: const ImageIcon( +// AssetImage('assets/logos/paperless_logo_green.png'), +// ), +// applicationName: 'Paperless Mobile', +// applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, +// children: [ +// Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), +// Link( +// uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), +// builder: (context, followLink) => GestureDetector( +// onTap: followLink, +// child: Text( +// 'https://github.com/astubenbord/paperless-mobile', +// style: TextStyle(color: Theme.of(context).colorScheme.tertiary), +// ), +// ), +// ), +// const SizedBox(height: 16), +// Text( +// 'Credits', +// style: Theme.of(context).textTheme.titleMedium, +// ), +// _buildOnboardingImageCredits(), +// ], +// ); +// } - Widget _buildOnboardingImageCredits() { - return Link( - uri: Uri.parse( - 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), - builder: (context, followLink) => Wrap( - children: [ - const Text('Onboarding images by '), - GestureDetector( - onTap: followLink, - child: Text( - 'pch.vector', - style: TextStyle(color: Theme.of(context).colorScheme.tertiary), - ), - ), - const Text(' on Freepik.') - ], - ), - ); - } +// Widget _buildOnboardingImageCredits() { +// return Link( +// uri: Uri.parse( +// 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), +// builder: (context, followLink) => Wrap( +// children: [ +// const Text('Onboarding images by '), +// GestureDetector( +// onTap: followLink, +// child: Text( +// 'pch.vector', +// style: TextStyle(color: Theme.of(context).colorScheme.tertiary), +// ), +// ), +// const Text(' on Freepik.') +// ], +// ), +// ); +// } - List> _buildEntries( - BuildContext context) { - List> items = []; - for (final entry in displayedActions) { - switch (entry) { - case AppPopupMenuEntries.documentsSelectListView: - items.add(_buildListViewTile()); - break; - case AppPopupMenuEntries.documentsSelectGridView: - items.add(_buildGridViewTile()); - break; - case AppPopupMenuEntries.openAboutThisAppDialog: - items.add(_buildAboutTile(context)); - break; - case AppPopupMenuEntries.reportBug: - items.add(_buildReportBugTile(context)); - break; - case AppPopupMenuEntries.openSettings: - items.add(_buildSettingsTile(context)); - break; - case AppPopupMenuEntries.divider: - items.add(const PopupMenuDivider()); - break; - } - } - return items; - } -} +// List> _buildEntries( +// BuildContext context) { +// List> items = []; +// for (final entry in displayedActions) { +// switch (entry) { +// case AppPopupMenuEntries.documentsSelectListView: +// items.add(_buildListViewTile()); +// break; +// case AppPopupMenuEntries.documentsSelectGridView: +// items.add(_buildGridViewTile()); +// break; +// case AppPopupMenuEntries.openAboutThisAppDialog: +// items.add(_buildAboutTile(context)); +// break; +// case AppPopupMenuEntries.reportBug: +// items.add(_buildReportBugTile(context)); +// break; +// case AppPopupMenuEntries.openSettings: +// items.add(_buildSettingsTile(context)); +// break; +// case AppPopupMenuEntries.divider: +// items.add(const PopupMenuDivider()); +// break; +// } +// } +// return items; +// } +// } diff --git a/lib/core/widgets/material/search/m3_search_bar.dart b/lib/core/widgets/material/search/m3_search_bar.dart index eb75144..dafcea1 100644 --- a/lib/core/widgets/material/search/m3_search_bar.dart +++ b/lib/core/widgets/material/search/m3_search_bar.dart @@ -39,7 +39,7 @@ class SearchBar extends StatelessWidget { surfaceTintColor: colorScheme.surfaceTint, borderRadius: BorderRadius.circular(effectiveHeight / 2), child: InkWell( - onTap: () {}, + onTap: onTap, borderRadius: BorderRadius.circular(effectiveHeight / 2), highlightColor: Colors.transparent, splashFactory: InkRipple.splashFactory, @@ -51,7 +51,9 @@ class SearchBar extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(right: 8), child: TextField( + onTap: onTap, readOnly: true, + enabled: false, cursorColor: colorScheme.primary, style: textTheme.bodyLarge, textAlignVertical: TextAlignVertical.center, @@ -64,7 +66,6 @@ class SearchBar extends StatelessWidget { color: colorScheme.onSurfaceVariant, ), ), - onTap: onTap, ), ), ), diff --git a/lib/core/widgets/paperless_logo.dart b/lib/core/widgets/paperless_logo.dart index 1f5b08b..38e3b0e 100644 --- a/lib/core/widgets/paperless_logo.dart +++ b/lib/core/widgets/paperless_logo.dart @@ -2,18 +2,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class PaperlessLogo extends StatelessWidget { + static const _paperlessGreen = Color(0xFF18541F); final double? height; final double? width; - final String _path; + final Color _color; - const PaperlessLogo.white({super.key, this.height, this.width}) - : _path = "assets/logos/paperless_logo_white.svg"; + const PaperlessLogo.white({ + super.key, + this.height, + this.width, + }) : _color = Colors.white; const PaperlessLogo.green({super.key, this.height, this.width}) - : _path = "assets/logos/paperless_logo_green.svg"; + : _color = _paperlessGreen; const PaperlessLogo.black({super.key, this.height, this.width}) - : _path = "assets/logos/paperless_logo_black.svg"; + : _color = Colors.black; + + const PaperlessLogo.colored(Color color, {super.key, this.height, this.width}) + : _color = color; @override Widget build(BuildContext context) { @@ -24,7 +31,8 @@ class PaperlessLogo extends StatelessWidget { ), padding: const EdgeInsets.only(right: 8), child: SvgPicture.asset( - _path, + "assets/logos/paperless_logo_white.svg", + color: _color, ), ); } diff --git a/lib/features/app_drawer/view/app_drawer.dart b/lib/features/app_drawer/view/app_drawer.dart index e69de29..0aae097 100644 --- a/lib/features/app_drawer/view/app_drawer.dart +++ b/lib/features/app_drawer/view/app_drawer.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/constants.dart'; +import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/features/settings/view/settings_page.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:url_launcher/link.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class AppDrawer extends StatelessWidget { + const AppDrawer({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + top: true, + child: Drawer( + child: Column( + children: [ + Row( + children: [ + const PaperlessLogo.green(), + Text( + "Paperless Mobile", + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ).padded(), + const Divider(), + ListTile( + dense: true, + title: Text(S.of(context).appDrawerAboutLabel), + leading: const Icon(Icons.info_outline), + onTap: () => _showAboutDialog(context), + ), + ListTile( + dense: true, + leading: const Icon(Icons.bug_report_outlined), + title: Text(S.of(context).appDrawerReportBugLabel), + onTap: () { + launchUrlString( + 'https://github.com/astubenbord/paperless-mobile/issues/new'); + }, + ), + ListTile( + dense: true, + leading: const Icon(Icons.settings_outlined), + title: Text( + S.of(context).appDrawerSettingsLabel, + ), + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: context.read(), + child: const SettingsPage(), + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _showAboutDialog(BuildContext context) { + showAboutDialog( + context: context, + applicationIcon: const ImageIcon( + AssetImage('assets/logos/paperless_logo_green.png'), + ), + applicationName: 'Paperless Mobile', + applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber, + children: [ + Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')), + Link( + uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'), + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + 'https://github.com/astubenbord/paperless-mobile', + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Credits', + style: Theme.of(context).textTheme.titleMedium, + ), + _buildOnboardingImageCredits(), + ], + ); + } + + Widget _buildOnboardingImageCredits() { + return Link( + uri: Uri.parse( + 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'), + builder: (context, followLink) => Wrap( + children: [ + const Text('Onboarding images by '), + GestureDetector( + onTap: followLink, + child: Text( + 'pch.vector', + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), + ), + ), + const Text(' on Freepik.') + ], + ), + ); + } +} diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart index 0752697..68772c8 100644 --- a/lib/features/document_details/bloc/document_details_cubit.dart +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -1,15 +1,10 @@ -import 'dart:developer'; import 'dart:io'; -import 'dart:typed_data'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; +import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:open_filex/open_filex.dart'; part 'document_details_state.dart'; 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 3a03f58..203ac30 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -482,7 +482,6 @@ class _DocumentDetailsPageState extends State { child: _DetailsItem( label: S.of(context).documentStoragePathPropertyLabel, content: StoragePathWidget( - isClickable: widget.isLabelClickable, pathId: document.storagePath, ), ).paddedSymmetrically(vertical: 16), diff --git a/lib/features/search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart similarity index 94% rename from lib/features/search/cubit/document_search_cubit.dart rename to lib/features/document_search/cubit/document_search_cubit.dart index 390c68b..2707926 100644 --- a/lib/features/search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; -import 'package:paperless_mobile/features/search/cubit/document_search_state.dart'; class DocumentSearchCubit extends HydratedCubit with PagedDocumentsMixin { diff --git a/lib/features/search/cubit/document_search_state.dart b/lib/features/document_search/cubit/document_search_state.dart similarity index 95% rename from lib/features/search/cubit/document_search_state.dart rename to lib/features/document_search/cubit/document_search_state.dart index 6667d7f..231405d 100644 --- a/lib/features/search/cubit/document_search_state.dart +++ b/lib/features/document_search/cubit/document_search_state.dart @@ -27,11 +27,8 @@ class DocumentSearchState extends PagedDocumentsState { }); @override - List get props => [ - hasLoaded, - isLoading, - filter, - value, + List get props => [ + ...super.props, searchHistory, suggestions, view, diff --git a/lib/features/search/cubit/document_search_state.g.dart b/lib/features/document_search/cubit/document_search_state.g.dart similarity index 100% rename from lib/features/search/cubit/document_search_state.g.dart rename to lib/features/document_search/cubit/document_search_state.g.dart diff --git a/lib/features/search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart similarity index 85% rename from lib/features/search/view/document_search_page.dart rename to lib/features/document_search/view/document_search_page.dart index a7255a2..9608dfb 100644 --- a/lib/features/search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -1,11 +1,13 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; +import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; -import 'package:paperless_mobile/features/search/cubit/document_search_cubit.dart'; -import 'package:paperless_mobile/features/search/cubit/document_search_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; Future showDocumentSearchPage(BuildContext context) { return Navigator.of(context).push( @@ -151,12 +153,27 @@ class _DocumentSearchPageState extends State { isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, + onTap: (document) async { + final updatedDocument = await Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, + ), + ) as DocumentModel?; + if (updatedDocument != document) { + context.read().reload(); + } + }, ) ], ); } void _selectSuggestion(String suggestion) { + _queryController.text = suggestion; context.read().search(suggestion); + FocusScope.of(context).unfocus(); } } diff --git a/lib/features/search/view/documents_search_app_bar.dart b/lib/features/document_search/view/documents_search_app_bar.dart similarity index 100% rename from lib/features/search/view/documents_search_app_bar.dart rename to lib/features/document_search/view/documents_search_app_bar.dart diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index eefd2c6..51c6b02 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -12,10 +12,7 @@ class DocumentsCubit extends HydratedCubit @override final PaperlessDocumentsApi api; - final SavedViewRepository _savedViewRepository; - - DocumentsCubit(this.api, this._savedViewRepository) - : super(const DocumentsState()); + DocumentsCubit(this.api) : super(const DocumentsState()); Future bulkRemove(List documents) async { log("[DocumentsCubit] bulkRemove"); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 8f8c7ef..1f1c3d7 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -1,33 +1,28 @@ -import 'dart:developer'; - import 'package:badges/badges.dart' as b; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; -import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/view_actions.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart'; -import 'package:paperless_mobile/features/search/view/document_search_page.dart'; import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class DocumentFilterIntent { final DocumentFilter? filter; @@ -116,6 +111,7 @@ class _DocumentsPageState extends State }, builder: (context, connectivityState) { return Scaffold( + drawer: const AppDrawer(), floatingActionButton: BlocBuilder( builder: (context, state) { final appliedFiltersCount = state.filter.appliedFiltersCount; @@ -165,6 +161,7 @@ class _DocumentsPageState extends State context, ), sliver: SearchAppBar( + hintText: "Search documents", //TODO: INTL onOpenSearch: showDocumentSearchPage, bottom: TabBar( controller: _tabController, @@ -177,9 +174,12 @@ class _DocumentsPageState extends State ), ), ], - body: NotificationListener( + body: NotificationListener( onNotification: (notification) { final metrics = notification.metrics; + if (metrics.maxScrollExtent == 0) { + return true; + } final desiredTab = (metrics.pixels / metrics.maxScrollExtent).round(); if (metrics.axis == Axis.horizontal && @@ -414,29 +414,21 @@ class _DocumentsPageState extends State } Future _openDetails(DocumentModel document) async { - final updatedModel = await Navigator.of(context).push( - _buildDetailsPageRoute(document), - ); + final updatedModel = await Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + ), + ) as DocumentModel?; + // final updatedModel = await Navigator.of(context).push( + // _buildDetailsPageRoute(document), + // ); if (updatedModel != document) { context.read().reload(); } } - MaterialPageRoute _buildDetailsPageRoute( - DocumentModel document) { - return MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => DocumentDetailsCubit( - context.read(), - document, - ), - child: const LabelRepositoriesProvider( - child: DocumentDetailsPage(), - ), - ), - ); - } - void _addTagToFilter(int tagId) { try { final tagsQuery = diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index b033504..b0288b5 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -48,11 +48,18 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { int _currentIndex = 0; final DocumentScannerCubit _scannerCubit = DocumentScannerCubit(); + late final InboxCubit _inboxCubit; @override void initState() { super.initState(); _initializeData(context); + _inboxCubit = InboxCubit( + context.read(), + context.read(), + context.read(), + context.read(), + ); context.read().reload(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _listenForReceivedFiles(); @@ -147,6 +154,12 @@ class _HomePageState extends State { } } + @override + void dispose() { + _inboxCubit.close(); + super.dispose(); + } + @override Widget build(BuildContext context) { final destinations = [ @@ -182,28 +195,15 @@ class _HomePageState extends State { ), label: S.of(context).bottomNavInboxPageLabel, ), - // RouteDescription( - // icon: const Icon(Icons.settings_outlined), - // selectedIcon: Icon( - // Icons.settings, - // color: Theme.of(context).colorScheme.primary, - // ), - // label: S.of(context).appDrawerSettingsLabel, - // ), ]; final routes = [ MultiBlocProvider( providers: [ BlocProvider( - create: (context) => DocumentsCubit( - context.read(), - context.read(), - ), + create: (context) => DocumentsCubit(context.read()), ), BlocProvider( - create: (context) => SavedViewCubit( - context.read(), - ), + create: (context) => SavedViewCubit(context.read()), ), ], child: const DocumentsPage(), @@ -213,13 +213,8 @@ class _HomePageState extends State { child: const ScannerPage(), ), const LabelsPage(), - BlocProvider( - create: (context) => InboxCubit( - context.read(), - context.read(), - context.read(), - context.read(), - ), + BlocProvider.value( + value: _inboxCubit, child: const InboxPage(), ), // const SettingsPage(), @@ -249,7 +244,6 @@ class _HomePageState extends State { builder: (context, sizingInformation) { if (!sizingInformation.isMobile) { return Scaffold( - // drawer: const AppDrawer(), body: Row( children: [ NavigationRail( diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index d70f5f2..b607538 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart'; @@ -11,6 +13,7 @@ import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart'; +import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; @@ -54,39 +57,8 @@ class _InboxPageState extends State { @override Widget build(BuildContext context) { - const _progressBarHeight = 4.0; return Scaffold( - appBar: PreferredSize( - preferredSize: - const Size.fromHeight(kToolbarHeight + _progressBarHeight), - child: BlocBuilder( - builder: (context, state) { - return AppBar( - title: Text(S.of(context).bottomNavInboxPageLabel), - actions: [ - if (state.hasLoaded) - Align( - alignment: Alignment.centerRight, - child: ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: ColoredBox( - color: Theme.of(context).colorScheme.secondaryContainer, - child: Text( - (state.value.isEmpty - ? '0 ' - : '${state.value.first.count} ') + - S.of(context).inboxPageUnseenText, - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.bodySmall, - ).paddedSymmetrically(horizontal: 4.0), - ), - ), - ).paddedSymmetrically(horizontal: 8) - ], - ); - }, - ), - ), + drawer: const AppDrawer(), floatingActionButton: BlocBuilder( builder: (context, state) { if (!state.hasLoaded || state.documents.isEmpty) { @@ -104,91 +76,95 @@ class _InboxPageState extends State { ); }, ), - body: BlocBuilder( - builder: (context, state) { - if (!state.hasLoaded) { - return const DocumentsListLoadingWidget(); - } + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SearchAppBar( + hintText: "Search documents", + onOpenSearch: showDocumentSearchPage, + ), + ], + body: BlocBuilder( + builder: (context, state) { + if (!state.hasLoaded) { + return const CustomScrollView( + physics: NeverScrollableScrollPhysics(), + slivers: [DocumentsListLoadingWidget()], + ); + } - if (state.documents.isEmpty) { - return InboxEmptyWidget( - emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, - ); - } + if (state.documents.isEmpty) { + return InboxEmptyWidget( + emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, + ); + } - // Build a list of slivers alternating between SliverToBoxAdapter - // (group header) and a SliverList (inbox items). - final List slivers = _groupByDate(state.documents) - .entries - .map( - (entry) => [ - SliverToBoxAdapter( - child: Align( - alignment: Alignment.centerLeft, - child: ClipRRect( - borderRadius: BorderRadius.circular(32.0), - child: Text( - entry.key, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ).padded(), - ), - ).paddedOnly(top: 8.0), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: entry.value.length, - (context, index) { - if (index < entry.value.length - 1) { - return Column( - children: [ - _buildListItem( - entry.value[index], - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ], + // Build a list of slivers alternating between SliverToBoxAdapter + // (group header) and a SliverList (inbox items). + final List slivers = _groupByDate(state.documents) + .entries + .map( + (entry) => [ + SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerLeft, + child: ClipRRect( + borderRadius: BorderRadius.circular(32.0), + child: Text( + entry.key, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ).padded(), + ), + ).paddedOnly(top: 8.0), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: entry.value.length, + (context, index) { + if (index < entry.value.length - 1) { + return Column( + children: [ + _buildListItem( + entry.value[index], + ), + const Divider( + indent: 16, + endIndent: 16, + ), + ], + ); + } + return _buildListItem( + entry.value[index], ); - } - return _buildListItem( - entry.value[index], - ); - }, + }, + ), + ), + ], + ) + .flattened + .toList() + ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); + + return RefreshIndicator( + onRefresh: () => context.read().initializeInbox(), + child: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverToBoxAdapter( + child: HintCard( + show: !state.isHintAcknowledged, + hintText: S.of(context).inboxPageUsageHintText, + onHintAcknowledged: () => + context.read().acknowledgeHint(), ), ), + ...slivers, ], - ) - .flattened - .toList() - ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); - - return RefreshIndicator( - onRefresh: () => context.read().initializeInbox(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: CustomScrollView( - controller: _scrollController, - slivers: [ - SliverToBoxAdapter( - child: HintCard( - show: !state.isHintAcknowledged, - hintText: S.of(context).inboxPageUsageHintText, - onHintAcknowledged: () => - context.read().acknowledgeHint(), - ), - ), - ...slivers, - ], - ), - ), - ], - ), - ); - }, + ), + ); + }, + ), ), ); } diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 2367594..0f5709e 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class InboxItem extends StatefulWidget { static const _a4AspectRatio = 1 / 1.4142; @@ -40,24 +41,16 @@ class _InboxItemState extends State { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { - final returnedDocument = await Navigator.push( + final updatedDocument = await Navigator.pushNamed( context, - MaterialPageRoute( - builder: (context) => BlocProvider( - create: (context) => DocumentDetailsCubit( - context.read(), - widget.document, - ), - child: const LabelRepositoriesProvider( - child: DocumentDetailsPage( - isLabelClickable: false, - ), - ), - ), + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: widget.document, + isLabelClickable: false, ), - ); - if (returnedDocument != null) { - widget.onDocumentUpdated(returnedDocument); + ) as DocumentModel?; + if (updatedDocument != null) { + widget.onDocumentUpdated(updatedDocument); } }, child: SizedBox( diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index c1e2a4f..3ea2ae4 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -266,6 +266,10 @@ class _TagFormFieldState extends State { Widget _buildNotAssignedTag(FormFieldState field) { return ColoredChipWrapper( child: InputChip( + labelPadding: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.all(4), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide.none, label: Text( S.of(context).labelNotAssignedText, ), @@ -288,6 +292,10 @@ class _TagFormFieldState extends State { } return ColoredChipWrapper( child: InputChip( + labelPadding: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.all(4), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide.none, label: Text( tag.name, style: TextStyle( @@ -312,6 +320,10 @@ class _TagFormFieldState extends State { Widget _buildAnyAssignedTag(FormFieldState field) { return ColoredChipWrapper( child: InputChip( + labelPadding: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.all(4), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide.none, label: Text(S.of(context).labelAnyAssignedText), backgroundColor: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.12), diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index f84bb3a..d64521c 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -8,6 +10,9 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; +import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; @@ -16,12 +21,13 @@ import 'package:paperless_mobile/features/edit_label/view/impl/edit_corresponden import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart'; -import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/storage_path_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart'; +import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class LabelsPage extends StatefulWidget { @@ -51,154 +57,264 @@ class _LabelsPageState extends State child: BlocBuilder( builder: (context, connectedState) { return Scaffold( - appBar: AppBar( - title: Text( - [ - S.of(context).labelsPageCorrespondentsTitleText, - S.of(context).labelsPageDocumentTypesTitleText, - S.of(context).labelsPageTagsTitleText, - S.of(context).labelsPageStoragePathTitleText - ][_currentIndex], - ), - actions: [ - IconButton( - onPressed: [ - _openAddCorrespondentPage, - _openAddDocumentTypePage, - _openAddTagPage, - _openAddStoragePathPage, - ][_currentIndex], - icon: const Icon(Icons.add), - ) + drawer: const AppDrawer(), + floatingActionButton: FloatingActionButton( + onPressed: [ + _openAddCorrespondentPage, + _openAddDocumentTypePage, + _openAddTagPage, + _openAddStoragePathPage, + ][_currentIndex], + child: Icon(Icons.add), + ), + body: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + // This widget takes the overlapping behavior of the SliverAppBar, + // and redirects it to the SliverOverlapInjector below. If it is + // missing, then it is possible for the nested "inner" scroll view + // below to end up under the SliverAppBar even when the inner + // scroll view thinks it has not been scrolled. + // This is not necessary if the "headerSliverBuilder" only builds + // widgets that do not overlap the next sliver. + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + sliver: SearchAppBar( + hintText: "Search documents", //TODO: INTL + onOpenSearch: showDocumentSearchPage, + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab( + icon: Icon( + Icons.person_outline, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + Tab( + icon: Icon( + Icons.description_outlined, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + Tab( + icon: Icon( + Icons.label_outline, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + Tab( + icon: Icon( + Icons.folder_open, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ], + ), + ), + ), ], - bottom: PreferredSize( - preferredSize: Size.fromHeight( - kToolbarHeight + (!connectedState.isConnected ? 16 : 0)), - child: Column( - children: [ - if (!connectedState.isConnected) const OfflineBanner(), - ColoredBox( - color: Theme.of(context).bottomAppBarColor, - child: TabBar( - indicatorColor: Theme.of(context).colorScheme.primary, - controller: _tabController, - tabs: [ - Tab( - icon: Icon( - Icons.person_outline, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - Tab( - icon: Icon( - Icons.description_outlined, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - Tab( - icon: Icon( - Icons.label_outline, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - Tab( - icon: Icon( - Icons.folder_open, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ) - ], + body: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.maxScrollExtent == 0) { + return true; + } + final desiredTab = + ((metrics.pixels / metrics.maxScrollExtent) * + (_tabController.length - 1)) + .round(); + + if (metrics.axis == Axis.horizontal && + _currentIndex != desiredTab) { + setState(() => _currentIndex = desiredTab); + } + return true; + }, + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), + ), + ), + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), + ), + ), + BlocProvider( + create: (context) => LabelCubit( + context + .read>(), + ), + ), + BlocProvider( + create: (context) => LabelCubit( + context.read< + LabelRepository>(), ), ), ], + child: RefreshIndicator( + edgeOffset: kToolbarHeight, + notificationPredicate: (notification) => + connectedState.isConnected, + onRefresh: () => [ + context.read>(), + context.read>(), + context.read>(), + context.read>(), + ][_currentIndex] + .reload(), + child: TabBarView( + controller: _tabController, + children: [ + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + correspondent: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditCorrespondentPage, + emptyStateActionButtonLabel: S + .of(context) + .labelsPageCorrespondentEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageCorrespondentEmptyStateDescriptionText, + onAddNew: _openAddCorrespondentPage, + ), + ], + ); + }, + ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + DocumentTypeBlocProvider( + child: LabelTabView( + filterBuilder: (label) => DocumentFilter( + documentType: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditDocumentTypePage, + emptyStateActionButtonLabel: S + .of(context) + .labelsPageDocumentTypeEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageDocumentTypeEmptyStateDescriptionText, + onAddNew: _openAddDocumentTypePage, + ), + ), + ], + ); + }, + ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + TagBlocProvider( + child: LabelTabView( + filterBuilder: (label) => DocumentFilter( + tags: IdsTagsQuery.fromIds([label.id!]), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditTagPage, + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag ?? false + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, + ), + emptyStateActionButtonLabel: S + .of(context) + .labelsPageTagsEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageTagsEmptyStateDescriptionText, + onAddNew: _openAddTagPage, + ), + ), + ], + ); + }, + ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + StoragePathBlocProvider( + child: LabelTabView( + onEdit: _openEditStoragePathPage, + filterBuilder: (label) => DocumentFilter( + storagePath: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + contentBuilder: (path) => + Text(path.path ?? ""), + emptyStateActionButtonLabel: S + .of(context) + .labelsPageStoragePathEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageStoragePathEmptyStateDescriptionText, + onAddNew: _openAddStoragePathPage, + ), + ), + ], + ); + }, + ), + ], + ), + ), ), ), ), - body: TabBarView( - controller: _tabController, - children: [ - CorrespondentBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - correspondent: IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditCorrespondentPage, - emptyStateActionButtonLabel: S - .of(context) - .labelsPageCorrespondentEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageCorrespondentEmptyStateDescriptionText, - onAddNew: _openAddCorrespondentPage, - ), - ), - DocumentTypeBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - documentType: IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditDocumentTypePage, - emptyStateActionButtonLabel: S - .of(context) - .labelsPageDocumentTypeEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageDocumentTypeEmptyStateDescriptionText, - onAddNew: _openAddDocumentTypePage, - ), - ), - TagBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - tags: IdsTagsQuery.fromIds([label.id!]), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar( - backgroundColor: t.color, - child: t.isInboxTag ?? false - ? Icon( - Icons.inbox, - color: t.textColor, - ) - : null, - ), - emptyStateActionButtonLabel: - S.of(context).labelsPageTagsEmptyStateAddNewLabel, - emptyStateDescription: - S.of(context).labelsPageTagsEmptyStateDescriptionText, - onAddNew: _openAddTagPage, - ), - ), - StoragePathBlocProvider( - child: LabelTabView( - onEdit: _openEditStoragePathPage, - filterBuilder: (label) => DocumentFilter( - storagePath: IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - contentBuilder: (path) => Text(path.path ?? ""), - emptyStateActionButtonLabel: S - .of(context) - .labelsPageStoragePathEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageStoragePathEmptyStateDescriptionText, - onAddNew: _openAddStoragePathPage, - ), - ), - ], - ), ); }, ), diff --git a/lib/features/labels/view/widgets/label_tab_view.dart b/lib/features/labels/view/widgets/label_tab_view.dart index 76133a6..8aecad4 100644 --- a/lib/features/labels/view/widgets/label_tab_view.dart +++ b/lib/features/labels/view/widgets/label_tab_view.dart @@ -46,45 +46,46 @@ class LabelTabView extends StatelessWidget { } final labels = state.labels.values.toList()..sort(); if (labels.isEmpty) { - return Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - emptyStateDescription, - textAlign: TextAlign.center, - ), - TextButton( - onPressed: onAddNew, - child: Text(emptyStateActionButtonLabel), - ), - ].padded(), + return SliverFillRemaining( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + emptyStateDescription, + textAlign: TextAlign.center, + ), + TextButton( + onPressed: onAddNew, + child: Text(emptyStateActionButtonLabel), + ), + ].padded(), + ), ), ); } - return RefreshIndicator( - onRefresh: context.read>().reload, - notificationPredicate: (notification) => - connectivityState.isConnected, - child: ListView( - children: labels - .map((l) => LabelItem( - name: l.name, - content: contentBuilder?.call(l) ?? - Text( - translateMatchingAlgorithmName( - context, l.matchingAlgorithm) + - ((l.match?.isNotEmpty ?? false) - ? ": ${l.match}" - : ""), - maxLines: 2, - ), - onOpenEditPage: onEdit, - filterBuilder: filterBuilder, - leading: leadingBuilder?.call(l), - label: l, - )) - .toList(), + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final l = labels.elementAt(index); + return LabelItem( + name: l.name, + content: contentBuilder?.call(l) ?? + Text( + translateMatchingAlgorithmName( + context, l.matchingAlgorithm) + + ((l.match?.isNotEmpty ?? false) + ? ": ${l.match}" + : ""), + maxLines: 2, + ), + onOpenEditPage: onEdit, + filterBuilder: filterBuilder, + leading: leadingBuilder?.call(l), + label: l, + ); + }, + childCount: labels.length, ), ); }, diff --git a/lib/features/linked_documents/view/pages/linked_documents_page.dart b/lib/features/linked_documents/view/pages/linked_documents_page.dart index b0956c2..7724a01 100644 --- a/lib/features/linked_documents/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents/view/pages/linked_documents_page.dart @@ -11,6 +11,7 @@ import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class LinkedDocumentsPage extends StatefulWidget { const LinkedDocumentsPage({super.key}); @@ -59,6 +60,19 @@ class _LinkedDocumentsPageState extends State { isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, + onTap: (document) async { + final updatedDocument = await Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, + ), + ) as DocumentModel?; + if (updatedDocument != document) { + context.read().reload(); + } + }, ); }, ); diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart index 04f09ef..c6648c2 100644 --- a/lib/features/saved_view/view/saved_view_list.dart +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -29,7 +29,8 @@ class SavedViewList extends StatelessWidget { return ListTile( title: Text(view.name), subtitle: Text( - "${view.filterRules.length} filter(s) set"), //TODO: INTL w/ placeholder + "${view.filterRules.length} filter(s) set", + ), //TODO: INTL w/ placeholder onTap: () { Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/features/saved_view/view/saved_view_page.dart b/lib/features/saved_view/view/saved_view_page.dart index 84f7b20..cd3aa0f 100644 --- a/lib/features/saved_view/view/saved_view_page.dart +++ b/lib/features/saved_view/view/saved_view_page.dart @@ -12,6 +12,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/view_actions.da import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class SavedViewPage extends StatefulWidget { final Future Function(SavedView savedView) onDelete; @@ -101,6 +102,12 @@ class _SavedViewPageState extends State { onTap: _onOpenDocumentDetails, viewType: _viewType, ), + if (state.hasLoaded && state.isLoading) + const SliverToBoxAdapter( + child: Center( + child: CircularProgressIndicator(), + ), + ) ], ); }, @@ -111,20 +118,14 @@ class _SavedViewPageState extends State { } void _onOpenDocumentDetails(DocumentModel document) async { - final updatedDocument = await Navigator.push( + final updatedDocument = await Navigator.pushNamed( context, - MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => DocumentDetailsCubit( - context.read(), - document, - ), - child: const LabelRepositoriesProvider( - child: DocumentDetailsPage(), - ), - ), + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, ), - ); + ) as DocumentModel?; if (updatedDocument != document) { // Reload in case document was edited and might not fulfill filter criteria of saved view anymore context.read().reload(); diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index bfa8fc4..9f8740a 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -17,12 +17,14 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; -import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/widgets/scanned_image_item.dart'; +import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/file_helpers.dart'; @@ -47,20 +49,100 @@ class _ScannerPageState extends State return BlocBuilder( builder: (context, connectedState) { return Scaffold( + drawer: const AppDrawer(), floatingActionButton: FloatingActionButton( onPressed: () => _openDocumentScanner(context), child: const Icon(Icons.add_a_photo_outlined), ), - appBar: _buildAppBar(context, connectedState.isConnected), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: _buildBody(connectedState.isConnected), + //appBar: _buildAppBar(context, connectedState.isConnected), + // body: Padding( + // padding: const EdgeInsets.all(8.0), + // child: _buildBody(connectedState.isConnected), + // ), + body: BlocBuilder>( + builder: (context, state) { + return NestedScrollView( + floatHeaderSlivers: false, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SearchAppBar( + hintText: "Search documents", //TODO: INTL + onOpenSearch: showDocumentSearchPage, + bottom: PreferredSize( + child: _buildActions(connectedState.isConnected), + preferredSize: const Size.fromHeight(kTextTabBarHeight), + ), + ), + ], + body: CustomScrollView( + slivers: [ + if (state.isEmpty) + SliverFillRemaining( + child: _buildEmptyState(connectedState.isConnected), + ) + else + _buildImageGrid(state) + ], + ), + ); + }, ), ); }, ); } + Widget _buildActions(bool isConnected) { + return SizedBox( + height: kTextTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + BlocBuilder>( + builder: (context, state) { + return TextButton.icon( + label: Text("Preview"), //TODO: INTL + onPressed: state.isNotEmpty + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DocumentView( + documentBytes: _assembleFileBytes( + state, + forcePdf: true, + ).then((file) => file.bytes), + ), + ), + ) + : null, + icon: const Icon(Icons.visibility_outlined), + ); + }, + ), + BlocBuilder>( + builder: (context, state) { + return TextButton.icon( + label: Text("Clear all"), //TODO: INTL + onPressed: state.isEmpty ? null : () => _reset(context), + icon: const Icon(Icons.delete_sweep_outlined), + ); + }, + ), + BlocBuilder>( + builder: (context, state) { + return TextButton.icon( + label: Text("Upload"), //TODO: INTL + onPressed: state.isEmpty || !isConnected + ? null + : () => _onPrepareDocumentUpload(context), + icon: const Icon(Icons.upload_outlined), + ); + }, + ), + ], + ), + ); + } + AppBar _buildAppBar(BuildContext context, bool isConnected) { return AppBar( title: Text(S.of(context).documentScannerPageTitle), @@ -170,7 +252,7 @@ class _ScannerPageState extends State } } - Widget _buildBody(bool isConnected) { + Widget _buildEmptyState(bool isConnected) { return BlocBuilder>( builder: (context, scans) { if (scans.isNotEmpty) { @@ -207,7 +289,7 @@ class _ScannerPageState extends State } Widget _buildImageGrid(List scans) { - return GridView.builder( + return SliverGrid.builder( itemCount: scans.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, diff --git a/lib/features/search_app_bar/view/search_app_bar.dart b/lib/features/search_app_bar/view/search_app_bar.dart index 08ddb62..ee70481 100644 --- a/lib/features/search_app_bar/view/search_app_bar.dart +++ b/lib/features/search_app_bar/view/search_app_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart'; typedef OpenSearchCallback = void Function(BuildContext context); @@ -8,11 +9,13 @@ class SearchAppBar extends StatefulWidget with PreferredSizeWidget { final PreferredSizeWidget? bottom; final OpenSearchCallback onOpenSearch; final Color? backgroundColor; + final String hintText; const SearchAppBar({ super.key, required this.onOpenSearch, this.bottom, this.backgroundColor, + required this.hintText, }); @override @@ -26,25 +29,29 @@ class _SearchAppBarState extends State { @override Widget build(BuildContext context) { return SliverAppBar( + automaticallyImplyLeading: false, floating: true, pinned: true, snap: true, backgroundColor: widget.backgroundColor, title: SearchBar( height: kToolbarHeight - 8, - supportingText: "Search documents", + supportingText: widget.hintText, onTap: () => widget.onOpenSearch(context), leadingIcon: IconButton( icon: const Icon(Icons.menu), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, + onPressed: Scaffold.of(context).openDrawer, ), trailingIcon: IconButton( icon: const CircleAvatar( child: Text("A"), ), - onPressed: () {}, + onPressed: () { + showDialog( + context: context, + builder: (context) => AccountSettingsDialog(), + ); + }, ), ).paddedOnly(top: 4, bottom: 4), bottom: widget.bottom, diff --git a/lib/features/settings/view/dialogs/account_settings_dialog.dart b/lib/features/settings/view/dialogs/account_settings_dialog.dart new file mode 100644 index 0000000..530078b --- /dev/null +++ b/lib/features/settings/view/dialogs/account_settings_dialog.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; +import 'package:paperless_mobile/core/widgets/hint_card.dart'; +import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; +import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; + +class AccountSettingsDialog extends StatelessWidget { + const AccountSettingsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + scrollable: true, + contentPadding: EdgeInsets.zero, + icon: const PaperlessLogo.green(), + title: const Text(" Your Accounts"), + content: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + ExpansionTile( + leading: CircleAvatar( + child: Text(state.information?.username + ?.toUpperCase() + .substring(0, 1) ?? + ''), + ), + title: Text(state.information?.username ?? ''), + subtitle: Text(state.information?.host ?? ''), + children: const [ + HintCard( + hintText: "WIP: Coming soon with multi user support!", + ), + ], + ), + Divider(), + ListTile( + dense: true, + leading: const Icon(Icons.person_add_rounded), + title: const Text("Add another account"), //TODO: INTL + onTap: () {}, + ), + Divider(), + OutlinedButton( + child: Text( + S.of(context).appDrawerLogoutLabel, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + onPressed: () async { + await _onLogout(context); + Navigator.of(context).maybePop(); + }, + ), + ], + ); + }, + ), + actions: [ + TextButton( + child: Text(S.of(context).genericActionCloseLabel), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } + + Future _onLogout(BuildContext context) async { + try { + await context.read().logout(); + await context.read().clear(); + await context.read>().clear(); + await context + .read>() + .clear(); + await context + .read>() + .clear(); + await context + .read>() + .clear(); + await context.read().clear(); + await HydratedBloc.storage.clear(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } +} diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index f29a4db..2fffad6 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -26,16 +26,6 @@ class SettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(S.of(context).appDrawerSettingsLabel), - actions: [ - IconButton( - icon: const Icon(Icons.logout), - color: Theme.of(context).colorScheme.error, - onPressed: () async { - await _onLogout(context); - Navigator.pop(context); - }, - ), - ], ), bottomNavigationBar: BlocBuilder( @@ -48,14 +38,16 @@ class SettingsPage extends StatelessWidget { " " + (info.username ?? 'unknown') + "@${info.host}", - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.labelSmall, + textAlign: TextAlign.center, ), subtitle: Text( S.of(context).serverInformationPaperlessVersionText + ' ' + info.version.toString() + ' (API v${info.apiVersion})', - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.labelSmall, + textAlign: TextAlign.center, ), ); }, @@ -100,25 +92,4 @@ class SettingsPage extends StatelessWidget { ), ); } - - Future _onLogout(BuildContext context) async { - try { - await context.read().logout(); - await context.read().clear(); - await context.read>().clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); - await context.read().clear(); - await HydratedBloc.storage.clear(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } } diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 8751050..b83ca4b 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -627,5 +627,6 @@ "verifyIdentityPageTitle": "Ověř svou identitu", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "genericActionCloseLabel": "Close" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 2f5c5e7..3c75572 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -627,5 +627,6 @@ "verifyIdentityPageTitle": "Verifiziere deine Identität", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "genericActionCloseLabel": "Close" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 54de836..a90f39b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -627,5 +627,6 @@ "verifyIdentityPageTitle": "Verify your identity", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "genericActionCloseLabel": "Close" } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 98d0d23..f9a6e61 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -627,5 +627,6 @@ "verifyIdentityPageTitle": "Kimliğinizi doğrulayın", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula", - "@verifyIdentityPageVerifyIdentityButtonLabel": {} + "@verifyIdentityPageVerifyIdentityButtonLabel": {}, + "genericActionCloseLabel": "Close" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 6cf6736..4f51e8e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -49,6 +49,7 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_sta import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; import 'package:paperless_mobile/theme.dart'; import 'package:paperless_mobile/constants.dart'; import 'package:path_provider/path_provider.dart'; @@ -254,6 +255,10 @@ class _PaperlessMobileEntrypointState extends State { GlobalWidgetsLocalizations.delegate, FormBuilderLocalizations.delegate, ], + routes: { + DocumentDetailsRoute.routeName: (context) => + const DocumentDetailsRoute(), + }, home: const AuthenticationWrapper(), ); }, diff --git a/lib/routes/document_details_route.dart b/lib/routes/document_details_route.dart new file mode 100644 index 0000000..36a0fad --- /dev/null +++ b/lib/routes/document_details_route.dart @@ -0,0 +1,46 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; + +class DocumentDetailsRoute extends StatelessWidget { + static const String routeName = "/documentDetails"; + const DocumentDetailsRoute({super.key}); + + @override + Widget build(BuildContext context) { + final args = ModalRoute.of(context)!.settings.arguments + as DocumentDetailsRouteArguments; + + return BlocProvider( + create: (context) => DocumentDetailsCubit( + context.read(), + args.document, + ), + child: LabelRepositoriesProvider( + child: DocumentDetailsPage( + allowEdit: args.allowEdit, + isLabelClickable: args.isLabelClickable, + titleAndContentQueryString: args.titleAndContentQueryString, + ), + ), + ); + } +} + +class DocumentDetailsRouteArguments { + final DocumentModel document; + final bool isLabelClickable; + final bool allowEdit; + final String? titleAndContentQueryString; + + DocumentDetailsRouteArguments({ + required this.document, + this.isLabelClickable = true, + this.allowEdit = true, + this.titleAndContentQueryString, + }); +} diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index 2c547c5..42e9609 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -84,9 +84,11 @@ class DocumentModel extends Equatable { id: id, title: title ?? this.title, content: content ?? this.content, - documentType: documentType?.call() ?? this.documentType, - correspondent: correspondent?.call() ?? this.correspondent, - storagePath: storagePath?.call() ?? this.storagePath, + documentType: + documentType != null ? documentType.call() : this.documentType, + correspondent: + correspondent != null ? correspondent.call() : this.correspondent, + storagePath: storagePath != null ? storagePath.call() : this.storagePath, tags: tags ?? this.tags, created: created ?? this.created, modified: modified ?? this.modified, diff --git a/pubspec.lock b/pubspec.lock index 1157538..4bb00ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: "12047baeca0e01df93165ef33275b32119d72699ab9a49dc64c20e78f586f96d" + url: "https://pub.dev" + source: hosted + version: "5.0.4" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: de5bfbc02ae4eebb339dd90d325749ae7536e903f6513ef72b88954072d72b0e + url: "https://pub.dev" + source: hosted + version: "5.0.3" badges: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c50f2f0..d10f818 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ dependencies: responsive_builder: ^0.4.3 open_filex: ^4.3.2 dynamic_color: ^1.5.4 + auto_route: ^5.0.4 dev_dependencies: integration_test: @@ -102,6 +103,7 @@ dev_dependencies: flutter_lints: ^1.0.0 json_serializable: ^6.5.4 dart_code_metrics: ^5.4.0 + auto_route_generator: ^5.0.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From ba5a1fcbc7c05a712a7210c408d507681041e1b3 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Thu, 2 Feb 2023 19:52:44 +0100 Subject: [PATCH 14/25] Small refactorings to inbox --- lib/features/home/view/home_page.dart | 4 +- .../home/view/widget/_app_drawer.dart | 320 +++++++++++++++++ lib/features/home/view/widget/app_drawer.dart | 321 ------------------ lib/features/inbox/bloc/inbox_cubit.dart | 20 +- .../inbox/bloc/state/inbox_state.dart | 6 + lib/features/inbox/view/pages/inbox_page.dart | 5 +- 6 files changed, 347 insertions(+), 329 deletions(-) create mode 100644 lib/features/home/view/widget/_app_drawer.dart delete mode 100644 lib/features/home/view/widget/app_drawer.dart diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index b0288b5..b09a647 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -20,7 +20,7 @@ import 'package:paperless_mobile/features/document_upload/view/document_upload_p import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/route_description.dart'; -import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart'; +import 'package:paperless_mobile/features/home/view/widget/_app_drawer.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; @@ -59,6 +59,7 @@ class _HomePageState extends State { context.read(), context.read(), context.read(), + context.read(), ); context.read().reload(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { @@ -271,7 +272,6 @@ class _HomePageState extends State { destinations: destinations.map((e) => e.toNavigationDestination()).toList(), ), - drawer: const AppDrawer(), body: routes[_currentIndex], ); }, diff --git a/lib/features/home/view/widget/_app_drawer.dart b/lib/features/home/view/widget/_app_drawer.dart new file mode 100644 index 0000000..8f2001b --- /dev/null +++ b/lib/features/home/view/widget/_app_drawer.dart @@ -0,0 +1,320 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:hydrated_bloc/hydrated_bloc.dart'; +// import 'package:package_info_plus/package_info_plus.dart'; +// import 'package:paperless_api/paperless_api.dart'; +// import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; +// import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; +// import 'package:paperless_mobile/core/repository/label_repository.dart'; +// import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +// import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +// import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; +// import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; +// import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; +// import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; +// import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +// import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; +// import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; +// import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; +// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +// import 'package:paperless_mobile/features/settings/view/settings_page.dart'; +// import 'package:paperless_mobile/generated/l10n.dart'; +// import 'package:paperless_mobile/helpers/message_helpers.dart'; +// import 'package:paperless_mobile/constants.dart'; +// import 'package:url_launcher/link.dart'; +// import 'package:url_launcher/url_launcher_string.dart'; + +// class AppDrawer extends StatefulWidget { +// final VoidCallback? afterInboxClosed; + +// const AppDrawer({Key? key, this.afterInboxClosed}) : super(key: key); + +// @override +// State createState() => _AppDrawerState(); +// } + +// // enum NavigationDestinations { +// // inbox, +// // settings, +// // reportBug, +// // about, +// // logout; +// // } + +// class _AppDrawerState extends State { +// @override +// void initState() { +// super.initState(); +// } + +// @override +// Widget build(BuildContext context) { +// final listtTileShape = RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(32), +// ); +// // return NavigationDrawer( +// // selectedIndex: -1, +// // children: [ +// // Text( +// // "", +// // style: Theme.of(context).textTheme.titleSmall, +// // ).padded(16), +// // NavigationDrawerDestination( +// // icon: const Icon(Icons.inbox), +// // label: Text(S.of(context).bottomNavInboxPageLabel), +// // ), +// // NavigationDrawerDestination( +// // icon: const Icon(Icons.settings), +// // label: Text(S.of(context).appDrawerSettingsLabel), +// // ), +// // const Divider( +// // indent: 16, +// // ), +// // NavigationDrawerDestination( +// // icon: const Icon(Icons.bug_report), +// // label: Text(S.of(context).appDrawerReportBugLabel), +// // ), +// // NavigationDrawerDestination( +// // icon: const Icon(Icons.info_outline), +// // label: Text(S.of(context).appDrawerAboutLabel), +// // ), +// // ], +// // onDestinationSelected: (idx) { +// // final val = NavigationDestinations.values[idx - 1]; +// // switch (val) { +// // case NavigationDestinations.inbox: +// // _onOpenInbox(); +// // break; +// // case NavigationDestinations.settings: +// // _onOpenSettings(); +// // break; +// // case NavigationDestinations.reportBug: +// // launchUrlString( +// // 'https://github.com/astubenbord/paperless-mobile/issues/new', +// // ); +// // break; +// // case NavigationDestinations.about: +// // _onShowAboutDialog(); +// // break; +// // case NavigationDestinations.logout: +// // _onLogout(); +// // break; +// // } +// // }, +// // ); +// return SafeArea( +// top: true, +// child: ClipRRect( +// borderRadius: const BorderRadius.only( +// topRight: Radius.circular(16.0), +// bottomRight: Radius.circular(16.0), +// ), +// child: Drawer( +// shape: const RoundedRectangleBorder( +// borderRadius: BorderRadius.only( +// topRight: Radius.circular(16.0), +// bottomRight: Radius.circular(16.0), +// ), +// ), +// child: ListView( +// children: [ +// DrawerHeader( +// decoration: BoxDecoration( +// color: Theme.of(context).colorScheme.secondaryContainer, +// ), +// padding: const EdgeInsets.only( +// top: 8, +// left: 8, +// bottom: 0, +// right: 8, +// ), +// child: Column( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Row( +// children: [ +// Image.asset( +// 'assets/logos/paperless_logo_white.png', +// height: 32, +// width: 32, +// color: +// Theme.of(context).colorScheme.onPrimaryContainer, +// ).paddedOnly(right: 8.0), +// Text( +// S.of(context).appTitleText, +// style: Theme.of(context) +// .textTheme +// .headlineSmall +// ?.copyWith( +// color: Theme.of(context) +// .colorScheme +// .onPrimaryContainer, +// ), +// ), +// ], +// ), +// Align( +// alignment: Alignment.bottomRight, +// child: BlocBuilder( +// builder: (context, state) { +// if (!state.isLoaded) { +// return Container(); +// } +// final info = state.information!; +// return Column( +// crossAxisAlignment: CrossAxisAlignment.end, +// children: [ +// ListTile( +// contentPadding: EdgeInsets.zero, +// dense: true, +// title: Text( +// S.of(context).appDrawerHeaderLoggedInAsText + +// (info.username ?? '?'), +// style: Theme.of(context).textTheme.bodyMedium, +// overflow: TextOverflow.ellipsis, +// textAlign: TextAlign.end, +// maxLines: 1, +// ), +// subtitle: Column( +// crossAxisAlignment: CrossAxisAlignment.end, +// children: [ +// Text( +// state.information!.host ?? '', +// style: Theme.of(context) +// .textTheme +// .bodyMedium, +// overflow: TextOverflow.ellipsis, +// textAlign: TextAlign.end, +// maxLines: 1, +// ), +// Text( +// '${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})', +// style: +// Theme.of(context).textTheme.bodySmall, +// overflow: TextOverflow.ellipsis, +// textAlign: TextAlign.end, +// maxLines: 1, +// ), +// ], +// ), +// isThreeLine: true, +// ), +// ], +// ); +// }, +// ), +// ), +// ], +// ), +// ), +// ...[ +// ListTile( +// title: Text(S.of(context).bottomNavInboxPageLabel), +// leading: const Icon(Icons.inbox), +// onTap: () => _onOpenInbox(), +// shape: listtTileShape, +// ), +// ListTile( +// leading: const Icon(Icons.settings), +// shape: listtTileShape, +// title: Text( +// S.of(context).appDrawerSettingsLabel, +// ), +// onTap: () => Navigator.of(context).push( +// MaterialPageRoute( +// builder: (context) => BlocProvider.value( +// value: context.read(), +// child: const SettingsPage(), +// ), +// ), +// ), +// ), +// const Divider( +// indent: 16, +// endIndent: 16, +// ), +// ListTile( +// leading: const Icon(Icons.bug_report), +// title: Text(S.of(context).appDrawerReportBugLabel), +// onTap: () { +// launchUrlString( +// 'https://github.com/astubenbord/paperless-mobile/issues/new'); +// }, +// shape: listtTileShape, +// ), +// ListTile( +// title: Text(S.of(context).appDrawerAboutLabel), +// leading: Icon(Icons.info_outline_rounded), +// onTap: _onShowAboutDialog, +// shape: listtTileShape, +// ), +// ListTile( +// leading: const Icon(Icons.logout), +// title: Text(S.of(context).appDrawerLogoutLabel), +// shape: listtTileShape, +// onTap: () { +// _onLogout(); +// }, +// ) +// ], +// ], +// ), +// ), +// ), +// ); +// } + +// void _onLogout() async { +// try { +// await context.read().logout(); +// await context.read().clear(); +// await context.read>().clear(); +// await context +// .read>() +// .clear(); +// await context +// .read>() +// .clear(); +// await context +// .read>() +// .clear(); +// await context.read().clear(); +// await HydratedBloc.storage.clear(); +// } on PaperlessServerException catch (error, stackTrace) { +// showErrorMessage(context, error, stackTrace); +// } +// } + +// Future _onOpenInbox() async { +// await Navigator.of(context).push( +// MaterialPageRoute( +// builder: (_) => LabelRepositoriesProvider( +// child: BlocProvider( +// create: (context) => InboxCubit( +// context.read(), +// context.read(), +// context.read(), +// context.read(), +// )..initializeInbox(), +// child: const InboxPage(), +// ), +// ), +// ), +// ); +// widget.afterInboxClosed?.call(); +// } + +// void _onOpenSettings() { +// Navigator.of(context).push( +// MaterialPageRoute( +// builder: (context) => BlocProvider.value( +// value: context.read(), +// child: const SettingsPage(), +// ), +// ), +// ); +// } +// void _onShowAboutDialog() {} +// } diff --git a/lib/features/home/view/widget/app_drawer.dart b/lib/features/home/view/widget/app_drawer.dart deleted file mode 100644 index 344a928..0000000 --- a/lib/features/home/view/widget/app_drawer.dart +++ /dev/null @@ -1,321 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; -import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; -import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; -import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; -import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; -import 'package:paperless_mobile/features/settings/view/settings_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/constants.dart'; -import 'package:url_launcher/link.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -class AppDrawer extends StatefulWidget { - final VoidCallback? afterInboxClosed; - - const AppDrawer({Key? key, this.afterInboxClosed}) : super(key: key); - - @override - State createState() => _AppDrawerState(); -} - -// enum NavigationDestinations { -// inbox, -// settings, -// reportBug, -// about, -// logout; -// } - -class _AppDrawerState extends State { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - final listtTileShape = RoundedRectangleBorder( - borderRadius: BorderRadius.circular(32), - ); - // return NavigationDrawer( - // selectedIndex: -1, - // children: [ - // Text( - // "", - // style: Theme.of(context).textTheme.titleSmall, - // ).padded(16), - // NavigationDrawerDestination( - // icon: const Icon(Icons.inbox), - // label: Text(S.of(context).bottomNavInboxPageLabel), - // ), - // NavigationDrawerDestination( - // icon: const Icon(Icons.settings), - // label: Text(S.of(context).appDrawerSettingsLabel), - // ), - // const Divider( - // indent: 16, - // ), - // NavigationDrawerDestination( - // icon: const Icon(Icons.bug_report), - // label: Text(S.of(context).appDrawerReportBugLabel), - // ), - // NavigationDrawerDestination( - // icon: const Icon(Icons.info_outline), - // label: Text(S.of(context).appDrawerAboutLabel), - // ), - // ], - // onDestinationSelected: (idx) { - // final val = NavigationDestinations.values[idx - 1]; - // switch (val) { - // case NavigationDestinations.inbox: - // _onOpenInbox(); - // break; - // case NavigationDestinations.settings: - // _onOpenSettings(); - // break; - // case NavigationDestinations.reportBug: - // launchUrlString( - // 'https://github.com/astubenbord/paperless-mobile/issues/new', - // ); - // break; - // case NavigationDestinations.about: - // _onShowAboutDialog(); - // break; - // case NavigationDestinations.logout: - // _onLogout(); - // break; - // } - // }, - // ); - return SafeArea( - top: true, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topRight: Radius.circular(16.0), - bottomRight: Radius.circular(16.0), - ), - child: Drawer( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16.0), - bottomRight: Radius.circular(16.0), - ), - ), - child: ListView( - children: [ - DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - padding: const EdgeInsets.only( - top: 8, - left: 8, - bottom: 0, - right: 8, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Image.asset( - 'assets/logos/paperless_logo_white.png', - height: 32, - width: 32, - color: - Theme.of(context).colorScheme.onPrimaryContainer, - ).paddedOnly(right: 8.0), - Text( - S.of(context).appTitleText, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ], - ), - Align( - alignment: Alignment.bottomRight, - child: BlocBuilder( - builder: (context, state) { - if (!state.isLoaded) { - return Container(); - } - final info = state.information!; - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - dense: true, - title: Text( - S.of(context).appDrawerHeaderLoggedInAsText + - (info.username ?? '?'), - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - state.information!.host ?? '', - style: Theme.of(context) - .textTheme - .bodyMedium, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - Text( - '${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})', - style: - Theme.of(context).textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - maxLines: 1, - ), - ], - ), - isThreeLine: true, - ), - ], - ); - }, - ), - ), - ], - ), - ), - ...[ - ListTile( - title: Text(S.of(context).bottomNavInboxPageLabel), - leading: const Icon(Icons.inbox), - onTap: () => _onOpenInbox(), - shape: listtTileShape, - ), - ListTile( - leading: const Icon(Icons.settings), - shape: listtTileShape, - title: Text( - S.of(context).appDrawerSettingsLabel, - ), - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: context.read(), - child: const SettingsPage(), - ), - ), - ), - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ListTile( - leading: const Icon(Icons.bug_report), - title: Text(S.of(context).appDrawerReportBugLabel), - onTap: () { - launchUrlString( - 'https://github.com/astubenbord/paperless-mobile/issues/new'); - }, - shape: listtTileShape, - ), - ListTile( - title: Text(S.of(context).appDrawerAboutLabel), - leading: Icon(Icons.info_outline_rounded), - onTap: _onShowAboutDialog, - shape: listtTileShape, - ), - ListTile( - leading: const Icon(Icons.logout), - title: Text(S.of(context).appDrawerLogoutLabel), - shape: listtTileShape, - onTap: () { - _onLogout(); - }, - ) - ], - ], - ), - ), - ), - ); - } - - void _onLogout() async { - try { - await context.read().logout(); - await context.read().clear(); - await context.read>().clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); - await context.read().clear(); - await HydratedBloc.storage.clear(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - - Future _onOpenInbox() async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => LabelRepositoriesProvider( - child: BlocProvider( - create: (context) => InboxCubit( - context.read(), - context.read(), - context.read(), - context.read(), - )..initializeInbox(), - child: const InboxPage(), - ), - ), - ), - ); - widget.afterInboxClosed?.call(); - } - - void _onOpenSettings() { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: context.read(), - child: const SettingsPage(), - ), - ), - ); - } - - void _onShowAboutDialog() {} -} diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index 2d2651a..cef27e1 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -18,16 +18,20 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { final PaperlessDocumentsApi _documentsApi; + final PaperlessServerStatsApi _statsApi; + final List _subscriptions = []; @override PaperlessDocumentsApi get api => _documentsApi; + Timer? _taskTimer; InboxCubit( this._tagsRepository, this._documentsApi, this._correspondentRepository, this._documentTypeRepository, + this._statsApi, ) : super( InboxState( availableCorrespondents: @@ -60,6 +64,15 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { } }), ); + //TODO: Do this properly in a background task. + _taskTimer = Timer.periodic(const Duration(seconds: 5), (timer) { + refreshItemsInInboxCount(); + }); + } + + void refreshItemsInInboxCount() async { + final stats = await _statsApi.getServerStatistics(); + emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox)); } /// @@ -175,9 +188,10 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { @override Future close() { - _subscriptions.forEach((element) { - element.cancel(); - }); + _taskTimer?.cancel(); + for (var sub in _subscriptions) { + sub.cancel(); + } return super.close(); } } diff --git a/lib/features/inbox/bloc/state/inbox_state.dart b/lib/features/inbox/bloc/state/inbox_state.dart index be2aafc..a2a5814 100644 --- a/lib/features/inbox/bloc/state/inbox_state.dart +++ b/lib/features/inbox/bloc/state/inbox_state.dart @@ -16,6 +16,8 @@ class InboxState extends PagedDocumentsState { final Map availableCorrespondents; + final int itemsInInboxCount; + @JsonKey() final bool isHintAcknowledged; @@ -29,6 +31,7 @@ class InboxState extends PagedDocumentsState { this.availableTags = const {}, this.availableDocumentTypes = const {}, this.availableCorrespondents = const {}, + this.itemsInInboxCount = 0, }); @override @@ -43,6 +46,7 @@ class InboxState extends PagedDocumentsState { availableTags, availableDocumentTypes, availableCorrespondents, + itemsInInboxCount, ]; InboxState copyWith({ @@ -56,6 +60,7 @@ class InboxState extends PagedDocumentsState { Map? availableCorrespondents, Map? availableDocumentTypes, Map? suggestions, + int? itemsInInboxCount, }) { return InboxState( hasLoaded: hasLoaded ?? super.hasLoaded, @@ -69,6 +74,7 @@ class InboxState extends PagedDocumentsState { availableDocumentTypes ?? this.availableDocumentTypes, availableTags: availableTags ?? this.availableTags, filter: filter ?? super.filter, + itemsInInboxCount: itemsInInboxCount ?? this.itemsInInboxCount, ); } diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index b607538..301ac97 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -79,9 +79,8 @@ class _InboxPageState extends State { body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SearchAppBar( - hintText: "Search documents", - onOpenSearch: showDocumentSearchPage, - ), + hintText: "Search documents", + onOpenSearch: showDocumentSearchPage), ], body: BlocBuilder( builder: (context, state) { From 661dad95808293913f7acab70085c9eeca1cd62c Mon Sep 17 00:00:00 2001 From: Anton Stubenbord <79228196+astubenbord@users.noreply.github.com> Date: Thu, 2 Feb 2023 23:59:46 +0100 Subject: [PATCH 15/25] Adds troubleshooting for known issues --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 9381d68..8866661 100644 --- a/README.md +++ b/README.md @@ -227,3 +227,6 @@ Here are some impressions from the app! Made with [contrib.rocks](https://contrib.rocks). +## Troubleshooting +#### Suggestions are not selectable in any of the label form fields +This is a known issue and it has to do with accessibility features of Android. Password managers such as Bitwarden often caused this issue to occur. Luckily, this can be resolved by turning off the accessibility features in these apps. From 3f305ce1d6c1211ee03b3e6e0402d12cd099518b Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Fri, 3 Feb 2023 00:27:14 +0100 Subject: [PATCH 16/25] Added translations, fixed inbox, minor updates to pages --- lib/core/widgets/hint_card.dart | 17 +- .../view/document_search_page.dart | 8 +- .../documents/bloc/documents_cubit.dart | 4 +- .../view/pages/document_edit_page.dart | 2 +- .../documents/view/pages/documents_page.dart | 84 ++- lib/features/home/view/home_page.dart | 28 +- lib/features/home/view/route_description.dart | 4 +- lib/features/inbox/bloc/inbox_cubit.dart | 4 + lib/features/inbox/view/pages/inbox_page.dart | 145 ++-- .../labels/tags/view/widgets/tags_widget.dart | 4 +- .../labels/view/widgets/label_form_field.dart | 2 - .../saved_view/view/saved_view_list.dart | 18 +- lib/features/scan/view/scanner_page.dart | 8 +- .../view/dialogs/account_settings_dialog.dart | 27 +- .../widgets/language_selection_setting.dart | 4 +- lib/l10n/intl_cs.arb | 57 +- lib/l10n/intl_de.arb | 27 +- lib/l10n/intl_en.arb | 27 +- lib/l10n/intl_pl.arb | 651 ++++++++++++++++++ lib/l10n/intl_tr.arb | 23 +- lib/theme.dart | 5 + .../paperless_server_statistics_model.dart | 4 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- 24 files changed, 982 insertions(+), 177 deletions(-) create mode 100644 lib/l10n/intl_pl.arb diff --git a/lib/core/widgets/hint_card.dart b/lib/core/widgets/hint_card.dart index d32ff04..03a5120 100644 --- a/lib/core/widgets/hint_card.dart +++ b/lib/core/widgets/hint_card.dart @@ -36,13 +36,16 @@ class HintCard extends StatelessWidget { hintIcon, color: Theme.of(context).hintColor, ).padded(), - Align( - alignment: Alignment.center, - child: Text( - hintText, - softWrap: true, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall, + Padding( + padding: const EdgeInsets.all(8.0), + child: Align( + alignment: Alignment.center, + child: Text( + hintText, + softWrap: true, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), ), ), if (onHintAcknowledged != null) diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 9608dfb..71b94ad 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -51,7 +51,7 @@ class _DocumentSearchPageState extends State { hintStyle: theme.textTheme.bodyLarge?.apply( color: theme.colorScheme.onSurfaceVariant, ), - hintText: "Search documents", //TODO: INTL + hintText: S.of(context).documentSearchSearchDocuments, border: InputBorder.none, ), controller: _queryController, @@ -143,8 +143,10 @@ class _DocumentSearchPageState extends State { slivers: [ SliverToBoxAdapter(child: header), if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) - const SliverToBoxAdapter( - child: Center(child: Text("No documents found.")), //TODO: INTL + SliverToBoxAdapter( + child: Center( + child: Text(S.of(context).documentSearchNoMatchesFound), + ), ) else SliverAdaptiveDocumentsView( diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 51c6b02..41dc04b 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -47,9 +47,7 @@ class DocumentsCubit extends HydratedCubit ), ); } else { - emit( - state.copyWith(selection: [...state.selection, model]), - ); + emit(state.copyWith(selection: [...state.selection, model])); } } diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index b854897..9238a3e 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -291,7 +291,7 @@ class _DocumentEditPageState extends State { label: Text(S.of(context).documentCreatedPropertyLabel), ), initialValue: initialCreatedAtDate, - format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format + format: DateFormat.yMMMMd(), initialEntryMode: DatePickerEntryMode.calendar, ), if (_filteredSuggestions.hasSuggestedDates) diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 1f1c3d7..201f1b7 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; @@ -12,6 +13,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/adaptive_docume import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; @@ -19,6 +21,7 @@ import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart'; import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; @@ -147,7 +150,6 @@ class _DocumentsPageState extends State return false; }, child: NestedScrollView( - floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( // This widget takes the overlapping behavior of the SliverAppBar, @@ -160,17 +162,41 @@ class _DocumentsPageState extends State handle: NestedScrollView.sliverOverlapAbsorberHandleFor( context, ), - sliver: SearchAppBar( - hintText: "Search documents", //TODO: INTL - onOpenSearch: showDocumentSearchPage, - bottom: TabBar( - controller: _tabController, - isScrollable: true, - tabs: [ - Tab(text: S.of(context).documentsPageTitle), - Tab(text: S.of(context).savedViewsLabel), - ], - ), + sliver: BlocBuilder( + builder: (context, state) { + if (state.selection.isNotEmpty) { + return SliverAppBar( + floating: false, + pinned: true, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => context + .read() + .resetSelection(), + ), + title: Text( + "${state.selection.length} ${S.of(context).documentsSelectedText}", + ), + actions: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _onDelete(state), + ), + ], + ); + } + return SearchAppBar( + hintText: S.of(context).documentSearchSearchDocuments, + onOpenSearch: showDocumentSearchPage, + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: S.of(context).documentsPageTitle), + Tab(text: S.of(context).savedViewsLabel), + ], + ), + ); + }, ), ), ], @@ -186,7 +212,7 @@ class _DocumentsPageState extends State _currentTab != desiredTab) { setState(() => _currentTab = desiredTab); } - return true; + return false; }, child: NotificationListener( onNotification: (notification) { @@ -213,7 +239,7 @@ class _DocumentsPageState extends State ), ); } - return true; + return false; }, child: TabBarView( controller: _tabController, @@ -233,6 +259,7 @@ class _DocumentsPageState extends State .sliverOverlapAbsorberHandleFor( context), ), + _buildViewActions(), BlocBuilder( buildWhen: (previous, current) => !const ListEquality().equals( @@ -324,8 +351,33 @@ class _DocumentsPageState extends State ); } - //TODO: Add app bar... - void _onDelete(BuildContext context, DocumentsState documentsState) async { + Widget _buildViewActions() { + return SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SortDocumentsButton(), + BlocBuilder( + builder: (context, state) { + return IconButton( + icon: Icon( + state.preferredViewType == ViewType.list + ? Icons.grid_view_rounded + : Icons.list, + ), + onPressed: () => + context.read().setViewType( + state.preferredViewType.toggle(), + ), + ); + }, + ) + ], + ).paddedSymmetrically(horizontal: 8, vertical: 4), + ); + } + + void _onDelete(DocumentsState documentsState) async { final shouldDelete = await showDialog( context: context, builder: (context) => diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index b09a647..ae5311b 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -20,15 +20,14 @@ import 'package:paperless_mobile/features/document_upload/view/document_upload_p import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/route_description.dart'; -import 'package:paperless_mobile/features/home/view/widget/_app_drawer.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; +import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; -import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -189,13 +188,24 @@ class _HomePageState extends State { label: S.of(context).bottomNavLabelsPageLabel, ), RouteDescription( - icon: const Icon(Icons.inbox_outlined), - selectedIcon: Icon( - Icons.inbox, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context).bottomNavInboxPageLabel, - ), + icon: const Icon(Icons.inbox_outlined), + selectedIcon: Icon( + Icons.inbox, + color: Theme.of(context).colorScheme.primary, + ), + label: S.of(context).bottomNavInboxPageLabel, + badgeBuilder: (icon) => BlocBuilder( + bloc: _inboxCubit, + builder: (context, state) { + if (state.itemsInInboxCount > 0) { + return Badge.count( + count: state.itemsInInboxCount, + child: icon, + ); + } + return icon; + }, + )), ]; final routes = [ MultiBlocProvider( diff --git a/lib/features/home/view/route_description.dart b/lib/features/home/view/route_description.dart index 6fc36a6..367c41a 100644 --- a/lib/features/home/view/route_description.dart +++ b/lib/features/home/view/route_description.dart @@ -16,8 +16,8 @@ class RouteDescription { NavigationDestination toNavigationDestination() { return NavigationDestination( label: label, - icon: icon, - selectedIcon: selectedIcon, + icon: badgeBuilder?.call(icon) ?? icon, + selectedIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon, ); } diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index cef27e1..f35d57f 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -115,6 +115,7 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { document.copyWith(tags: updatedTags), ); await remove(document); + emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); return tagsToRemove; } @@ -129,6 +130,7 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { tags: {...document.tags, ...removedTags}, ); await _documentsApi.update(updatedDoc); + emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1)); return reload(); } @@ -147,6 +149,7 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { emit(state.copyWith( hasLoaded: true, value: [], + itemsInInboxCount: 0, )); } finally { emit(state.copyWith(isLoading: false)); @@ -160,6 +163,7 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { } else { // Remove document from inbox. remove(document); + emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); } } diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 301ac97..11cb7d2 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -76,78 +76,81 @@ class _InboxPageState extends State { ); }, ), - body: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SearchAppBar( - hintText: "Search documents", - onOpenSearch: showDocumentSearchPage), - ], - body: BlocBuilder( - builder: (context, state) { - if (!state.hasLoaded) { - return const CustomScrollView( - physics: NeverScrollableScrollPhysics(), - slivers: [DocumentsListLoadingWidget()], - ); - } + body: RefreshIndicator( + edgeOffset: 78, + onRefresh: () => context.read().initializeInbox(), + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SearchAppBar( + hintText: S.of(context).documentSearchSearchDocuments, + onOpenSearch: showDocumentSearchPage, + ), + ], + body: BlocBuilder( + builder: (context, state) { + if (!state.hasLoaded) { + return const CustomScrollView( + physics: NeverScrollableScrollPhysics(), + slivers: [DocumentsListLoadingWidget()], + ); + } - if (state.documents.isEmpty) { - return InboxEmptyWidget( - emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, - ); - } + if (state.documents.isEmpty) { + return InboxEmptyWidget( + emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, + ); + } - // Build a list of slivers alternating between SliverToBoxAdapter - // (group header) and a SliverList (inbox items). - final List slivers = _groupByDate(state.documents) - .entries - .map( - (entry) => [ - SliverToBoxAdapter( - child: Align( - alignment: Alignment.centerLeft, - child: ClipRRect( - borderRadius: BorderRadius.circular(32.0), - child: Text( - entry.key, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ).padded(), - ), - ).paddedOnly(top: 8.0), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: entry.value.length, - (context, index) { - if (index < entry.value.length - 1) { - return Column( - children: [ - _buildListItem( - entry.value[index], - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ], - ); - } - return _buildListItem( - entry.value[index], - ); - }, + // Build a list of slivers alternating between SliverToBoxAdapter + // (group header) and a SliverList (inbox items). + final List slivers = _groupByDate(state.documents) + .entries + .map( + (entry) => [ + SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerLeft, + child: ClipRRect( + borderRadius: BorderRadius.circular(32.0), + child: Text( + entry.key, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ).padded(), + ), + ).paddedOnly(top: 8.0), ), - ), - ], - ) - .flattened - .toList() - ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: entry.value.length, + (context, index) { + if (index < entry.value.length - 1) { + return Column( + children: [ + _buildListItem( + entry.value[index], + ), + const Divider( + indent: 16, + endIndent: 16, + ), + ], + ); + } + return _buildListItem( + entry.value[index], + ); + }, + ), + ), + ], + ) + .flattened + .toList() + ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); + // edgeOffset: kToolbarHeight, - return RefreshIndicator( - onRefresh: () => context.read().initializeInbox(), - child: CustomScrollView( + return CustomScrollView( controller: _scrollController, slivers: [ SliverToBoxAdapter( @@ -160,9 +163,9 @@ class _InboxPageState extends State { ), ...slivers, ], - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/features/labels/tags/view/widgets/tags_widget.dart b/lib/features/labels/tags/view/widgets/tags_widget.dart index 49a41b8..63aa04f 100644 --- a/lib/features/labels/tags/view/widgets/tags_widget.dart +++ b/lib/features/labels/tags/view/widgets/tags_widget.dart @@ -51,9 +51,7 @@ class TagsWidget extends StatelessWidget { } else { return SingleChildScrollView( scrollDirection: Axis.horizontal, - child: Row( - children: children, - ), + child: Row(children: children), ); } }, diff --git a/lib/features/labels/view/widgets/label_form_field.dart b/lib/features/labels/view/widgets/label_form_field.dart index a43f18d..024576c 100644 --- a/lib/features/labels/view/widgets/label_form_field.dart +++ b/lib/features/labels/view/widgets/label_form_field.dart @@ -85,7 +85,6 @@ class _LabelFormFieldState extends State> { TextStyle(color: Theme.of(context).disabledColor, fontSize: 18.0), ), ), - getImmediateSuggestions: true, loadingBuilder: (context) => Container(), initialValue: widget.initialValue ?? const IdQueryParameter.unset(), name: widget.name, @@ -108,7 +107,6 @@ class _LabelFormFieldState extends State> { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - style: ListTileStyle.list, ), suggestionsCallback: (pattern) { final List suggestions = widget.labelOptions.entries diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart index c6648c2..0c4b7a4 100644 --- a/lib/features/saved_view/view/saved_view_list.dart +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; @@ -17,10 +17,11 @@ class SavedViewList extends StatelessWidget { return BlocBuilder( builder: (context, state) { if (state.value.isEmpty) { - return Text( - S.of(context).savedViewsEmptyStateText, - textAlign: TextAlign.center, - ).padded(); + return SliverToBoxAdapter( + child: HintCard( + hintText: S.of(context).savedViewsEmptyStateText, + ), + ); } return SliverList( delegate: SliverChildBuilderDelegate( @@ -29,8 +30,10 @@ class SavedViewList extends StatelessWidget { return ListTile( title: Text(view.name), subtitle: Text( - "${view.filterRules.length} filter(s) set", - ), //TODO: INTL w/ placeholder + S + .of(context) + .savedViewsFiltersSetCount(view.filterRules.length), + ), onTap: () { Navigator.of(context).push( MaterialPageRoute( @@ -42,7 +45,6 @@ class SavedViewList extends StatelessWidget { savedView: view, ), ), - BlocProvider.value(value: savedViewCubit), ], child: SavedViewPage( onDelete: savedViewCubit.remove, diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 9f8740a..29a3952 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -65,7 +65,7 @@ class _ScannerPageState extends State floatHeaderSlivers: false, headerSliverBuilder: (context, innerBoxIsScrolled) => [ SearchAppBar( - hintText: "Search documents", //TODO: INTL + hintText: S.of(context).documentSearchSearchDocuments, onOpenSearch: showDocumentSearchPage, bottom: PreferredSize( child: _buildActions(connectedState.isConnected), @@ -101,7 +101,7 @@ class _ScannerPageState extends State BlocBuilder>( builder: (context, state) { return TextButton.icon( - label: Text("Preview"), //TODO: INTL + label: Text(S.of(context).scannerPagePreviewLabel), onPressed: state.isNotEmpty ? () => Navigator.of(context).push( MaterialPageRoute( @@ -121,7 +121,7 @@ class _ScannerPageState extends State BlocBuilder>( builder: (context, state) { return TextButton.icon( - label: Text("Clear all"), //TODO: INTL + label: Text(S.of(context).scannerPageClearAllLabel), onPressed: state.isEmpty ? null : () => _reset(context), icon: const Icon(Icons.delete_sweep_outlined), ); @@ -130,7 +130,7 @@ class _ScannerPageState extends State BlocBuilder>( builder: (context, state) { return TextButton.icon( - label: Text("Upload"), //TODO: INTL + label: Text(S.of(context).scannerPageUploadLabel), onPressed: state.isEmpty || !isConnected ? null : () => _onPrepareDocumentUpload(context), diff --git a/lib/features/settings/view/dialogs/account_settings_dialog.dart b/lib/features/settings/view/dialogs/account_settings_dialog.dart index 530078b..d3ed33c 100644 --- a/lib/features/settings/view/dialogs/account_settings_dialog.dart +++ b/lib/features/settings/view/dialogs/account_settings_dialog.dart @@ -11,7 +11,7 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; -import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -25,8 +25,12 @@ class AccountSettingsDialog extends StatelessWidget { return AlertDialog( scrollable: true, contentPadding: EdgeInsets.zero, - icon: const PaperlessLogo.green(), - title: const Text(" Your Accounts"), + title: Row( + children: [ + const CloseButton(), + Text(S.of(context).accountSettingsTitle), + ], + ), content: BlocBuilder( builder: (context, state) { @@ -55,28 +59,27 @@ class AccountSettingsDialog extends StatelessWidget { onTap: () {}, ), Divider(), - OutlinedButton( + FilledButton( + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.error, + ), + ), child: Text( S.of(context).appDrawerLogoutLabel, style: TextStyle( - color: Theme.of(context).colorScheme.error, + color: Theme.of(context).colorScheme.onError, ), ), onPressed: () async { await _onLogout(context); Navigator.of(context).maybePop(); }, - ), + ).padded(16), ], ); }, ), - actions: [ - TextButton( - child: Text(S.of(context).genericActionCloseLabel), - onPressed: () => Navigator.pop(context), - ), - ], ); } diff --git a/lib/features/settings/view/widgets/language_selection_setting.dart b/lib/features/settings/view/widgets/language_selection_setting.dart index b141612..15a5ed3 100644 --- a/lib/features/settings/view/widgets/language_selection_setting.dart +++ b/lib/features/settings/view/widgets/language_selection_setting.dart @@ -47,11 +47,11 @@ class _LanguageSelectionSettingState extends State { ), RadioOption( value: 'cs', - label: _languageOptions['cs']! + " *", + label: _languageOptions['cs']! + "*", ), RadioOption( value: 'tr', - label: _languageOptions['tr']! + " *", + label: _languageOptions['tr']! + "*", ) ], initialValue: context diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index b83ca4b..ddfe2d9 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsTitle": "Account", + "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Nový korespondent", "@addCorrespondentPageTitle": {}, "addDocumentTypePageTitle": "Nový typ dokumentu", @@ -44,9 +46,9 @@ "@bottomNavLabelsPageLabel": {}, "bottomNavScannerPageLabel": "Skener", "@bottomNavScannerPageLabel": {}, - "colorSchemeOptionClassic": "Classic", + "colorSchemeOptionClassic": "Klasicky", "@colorSchemeOptionClassic": {}, - "colorSchemeOptionDynamic": "Dynamic", + "colorSchemeOptionDynamic": "Dynamicky", "@colorSchemeOptionDynamic": {}, "correspondentFormFieldSearchHintText": "Začni psát...", "@correspondentFormFieldSearchHintText": {}, @@ -68,23 +70,23 @@ "@documentDeleteSuccessMessage": {}, "documentDetailsPageAssignAsnButtonLabel": "Přiřadit", "@documentDetailsPageAssignAsnButtonLabel": {}, - "documentDetailsPageDeleteTooltip": "Delete", + "documentDetailsPageDeleteTooltip": "Smazat", "@documentDetailsPageDeleteTooltip": {}, - "documentDetailsPageDownloadTooltip": "Download", + "documentDetailsPageDownloadTooltip": "Stáhnout", "@documentDetailsPageDownloadTooltip": {}, - "documentDetailsPageEditTooltip": "Edit", + "documentDetailsPageEditTooltip": "Upravit", "@documentDetailsPageEditTooltip": {}, "documentDetailsPageLoadFullContentLabel": "Načíst celý obsah", "@documentDetailsPageLoadFullContentLabel": {}, - "documentDetailsPageNoPdfViewerFoundErrorMessage": "No app to display PDF files found!", + "documentDetailsPageNoPdfViewerFoundErrorMessage": "Aplikace pro otevírání PDF souborů nenalezena.", "@documentDetailsPageNoPdfViewerFoundErrorMessage": {}, - "documentDetailsPageOpenInSystemViewerTooltip": "Open in system viewer", + "documentDetailsPageOpenInSystemViewerTooltip": "Otevřít v systémovém prohlížeči", "@documentDetailsPageOpenInSystemViewerTooltip": {}, - "documentDetailsPageOpenPdfPermissionDeniedErrorMessage": "Could not open file: Permission denied.", + "documentDetailsPageOpenPdfPermissionDeniedErrorMessage": "Soubor nelze otevřít: přístup zamítnut.", "@documentDetailsPageOpenPdfPermissionDeniedErrorMessage": {}, - "documentDetailsPagePreviewTooltip": "Preview", + "documentDetailsPagePreviewTooltip": "Náhled", "@documentDetailsPagePreviewTooltip": {}, - "documentDetailsPageShareTooltip": "Share", + "documentDetailsPageShareTooltip": "Sdílet", "@documentDetailsPageShareTooltip": {}, "documentDetailsPageSimilarDocumentsLabel": "Podobné dokumenty", "@documentDetailsPageSimilarDocumentsLabel": {}, @@ -94,7 +96,7 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Přehled", "@documentDetailsPageTabOverviewLabel": {}, - "documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents", + "documentDetailsPageTabSimilarDocumentsLabel": "Podobné dokumenty", "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Typ dokumentu", "@documentDocumentTypePropertyLabel": {}, @@ -148,12 +150,16 @@ "@documentScannerPageUploadButtonTooltip": {}, "documentScannerPageUploadFromThisDeviceButtonLabel": "Nahrát jeden dokument z tohoto zařízení", "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, - "documentSearchHistory": "History", + "documentSearchHistory": "Historie", "@documentSearchHistory": {}, - "documentSearchPageRemoveFromHistory": "Remove from search history?", + "documentSearchNoMatchesFound": "No matches found.", + "@documentSearchNoMatchesFound": {}, + "documentSearchPageRemoveFromHistory": "Odstranit z historie vyhledávání?", "@documentSearchPageRemoveFromHistory": {}, - "documentSearchResults": "Results", + "documentSearchResults": "Výsledky", "@documentSearchResults": {}, + "documentSearchSearchDocuments": "Search documents", + "@documentSearchSearchDocuments": {}, "documentsEmptyStateResetFilterLabel": "Zrušit", "@documentsEmptyStateResetFilterLabel": {}, "documentsFilterPageAdvancedLabel": "Rozšířené", @@ -370,6 +376,8 @@ "@genericAcknowledgeLabel": {}, "genericActionCancelLabel": "Zrušit", "@genericActionCancelLabel": {}, + "genericActionCloseLabel": "Close", + "@genericActionCloseLabel": {}, "genericActionCreateLabel": "Vytvořit", "@genericActionCreateLabel": {}, "genericActionDeleteLabel": "Smazat", @@ -558,14 +566,26 @@ "@savedViewNameLabel": {}, "savedViewsEmptyStateText": "Vytvoře si různé náhledy pro rychlé filtrování dokumentů.", "@savedViewsEmptyStateText": {}, + "savedViewsFiltersSetCount": "{count, plural, zero{{count} filters set} one{{count} filter set} other{{count} filters set}}", + "@savedViewsFiltersSetCount": { + "placeholders": { + "count": {} + } + }, "savedViewShowInSidebarLabel": "Zobrazit v postranní liště", "@savedViewShowInSidebarLabel": {}, "savedViewShowOnDashboardLabel": "Zobrazit na hlavním panelu", "@savedViewShowOnDashboardLabel": {}, "savedViewsLabel": "Uložené náhledy", "@savedViewsLabel": {}, + "scannerPageClearAllLabel": "Clear all", + "@scannerPageClearAllLabel": {}, "scannerPageImagePreviewTitle": "Sken", "@scannerPageImagePreviewTitle": {}, + "scannerPagePreviewLabel": "Preview", + "@scannerPagePreviewLabel": {}, + "scannerPageUploadLabel": "Upload", + "@scannerPageUploadLabel": {}, "serverInformationPaperlessVersionText": "Verze Paperless serveru", "@serverInformationPaperlessVersionText": {}, "settingsPageAppearanceSettingDarkThemeLabel": "Tmavý vzhled", @@ -584,7 +604,7 @@ "@settingsPageColorSchemeSettingDialogDescription": {}, "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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.", "@settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": {}, - "settingsPageColorSchemeSettingLabel": "Colors", + "settingsPageColorSchemeSettingLabel": "Barvy", "@settingsPageColorSchemeSettingLabel": {}, "settingsPageLanguageSettingLabel": "Jazyk", "@settingsPageLanguageSettingLabel": {}, @@ -602,9 +622,9 @@ "@settingsThemeModeLightLabel": {}, "settingsThemeModeSystemLabel": "Systémový", "@settingsThemeModeSystemLabel": {}, - "sortDocumentAscending": "Ascending", + "sortDocumentAscending": "Vzestupně", "@sortDocumentAscending": {}, - "sortDocumentDescending": "Descending", + "sortDocumentDescending": "Sestupně", "@sortDocumentDescending": {}, "storagePathParameterDayLabel": "den", "@storagePathParameterDayLabel": {}, @@ -627,6 +647,5 @@ "verifyIdentityPageTitle": "Ověř svou identitu", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu", - "@verifyIdentityPageVerifyIdentityButtonLabel": {}, - "genericActionCloseLabel": "Close" + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 3c75572..6be28f4 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsTitle": "Account", + "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Neuer Korrespondent", "@addCorrespondentPageTitle": {}, "addDocumentTypePageTitle": "Neuer Dokumenttyp", @@ -150,10 +152,14 @@ "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, "documentSearchHistory": "Verlauf", "@documentSearchHistory": {}, + "documentSearchNoMatchesFound": "Keine Treffer.", + "@documentSearchNoMatchesFound": {}, "documentSearchPageRemoveFromHistory": "Aus dem Suchverlauf entfernen?", "@documentSearchPageRemoveFromHistory": {}, "documentSearchResults": "Ergebnisse", "@documentSearchResults": {}, + "documentSearchSearchDocuments": "Durchsuche Dokumente", + "@documentSearchSearchDocuments": {}, "documentsEmptyStateResetFilterLabel": "Filter zurücksetzen", "@documentsEmptyStateResetFilterLabel": {}, "documentsFilterPageAdvancedLabel": "Erweitert", @@ -268,7 +274,7 @@ "@errorMessageDocumentUploadFailed": {}, "errorMessageInvalidClientCertificateConfiguration": "Ungültiges Zertifikat oder fehlende Passphrase, bitte versuche es erneut.", "@errorMessageInvalidClientCertificateConfiguration": {}, - "errorMessageLoadSavedViewsError": "Gespeicherte Ansichten konnten nicht geladen werden.", + "errorMessageLoadSavedViewsError": "Ansichten konnten nicht geladen werden.", "@errorMessageLoadSavedViewsError": {}, "errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat.", "@errorMessageMissingClientCertificate": {}, @@ -370,6 +376,8 @@ "@genericAcknowledgeLabel": {}, "genericActionCancelLabel": "Abbrechen", "@genericActionCancelLabel": {}, + "genericActionCloseLabel": "Schließen", + "@genericActionCloseLabel": {}, "genericActionCreateLabel": "Erstellen", "@genericActionCreateLabel": {}, "genericActionDeleteLabel": "Löschen", @@ -558,14 +566,26 @@ "@savedViewNameLabel": {}, "savedViewsEmptyStateText": "Lege Ansichten an, um Dokumente schneller zu finden.", "@savedViewsEmptyStateText": {}, + "savedViewsFiltersSetCount": "{count, plural, zero{{count} Filter gesetzt} one{{count} Filter gesetzt} other{{count} Filter gesetzt}}", + "@savedViewsFiltersSetCount": { + "placeholders": { + "count": {} + } + }, "savedViewShowInSidebarLabel": "In Seitenleiste zeigen", "@savedViewShowInSidebarLabel": {}, "savedViewShowOnDashboardLabel": "Auf Startseite zeigen", "@savedViewShowOnDashboardLabel": {}, - "savedViewsLabel": "Gespeicherte Ansichten", + "savedViewsLabel": "Ansichten", "@savedViewsLabel": {}, + "scannerPageClearAllLabel": "Alle löschen", + "@scannerPageClearAllLabel": {}, "scannerPageImagePreviewTitle": "Aufnahme", "@scannerPageImagePreviewTitle": {}, + "scannerPagePreviewLabel": "Vorschau", + "@scannerPagePreviewLabel": {}, + "scannerPageUploadLabel": "Hochladen", + "@scannerPageUploadLabel": {}, "serverInformationPaperlessVersionText": "Paperless Server-Version", "@serverInformationPaperlessVersionText": {}, "settingsPageAppearanceSettingDarkThemeLabel": "Dunkler Modus", @@ -627,6 +647,5 @@ "verifyIdentityPageTitle": "Verifiziere deine Identität", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", - "@verifyIdentityPageVerifyIdentityButtonLabel": {}, - "genericActionCloseLabel": "Close" + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index a90f39b..8ab7807 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsTitle": "Account", + "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "New Correspondent", "@addCorrespondentPageTitle": {}, "addDocumentTypePageTitle": "New Document Type", @@ -150,10 +152,14 @@ "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, "documentSearchHistory": "History", "@documentSearchHistory": {}, + "documentSearchNoMatchesFound": "No matches found.", + "@documentSearchNoMatchesFound": {}, "documentSearchPageRemoveFromHistory": "Remove from search history?", "@documentSearchPageRemoveFromHistory": {}, "documentSearchResults": "Results", "@documentSearchResults": {}, + "documentSearchSearchDocuments": "Search documents", + "@documentSearchSearchDocuments": {}, "documentsEmptyStateResetFilterLabel": "Reset filter", "@documentsEmptyStateResetFilterLabel": {}, "documentsFilterPageAdvancedLabel": "Advanced", @@ -268,7 +274,7 @@ "@errorMessageDocumentUploadFailed": {}, "errorMessageInvalidClientCertificateConfiguration": "Invalid certificate or missing passphrase, please try again", "@errorMessageInvalidClientCertificateConfiguration": {}, - "errorMessageLoadSavedViewsError": "Could not load saved views.", + "errorMessageLoadSavedViewsError": "Could not load views.", "@errorMessageLoadSavedViewsError": {}, "errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate.", "@errorMessageMissingClientCertificate": {}, @@ -370,6 +376,8 @@ "@genericAcknowledgeLabel": {}, "genericActionCancelLabel": "Cancel", "@genericActionCancelLabel": {}, + "genericActionCloseLabel": "Close", + "@genericActionCloseLabel": {}, "genericActionCreateLabel": "Create", "@genericActionCreateLabel": {}, "genericActionDeleteLabel": "Delete", @@ -558,14 +566,26 @@ "@savedViewNameLabel": {}, "savedViewsEmptyStateText": "Create views to quickly filter your documents.", "@savedViewsEmptyStateText": {}, + "savedViewsFiltersSetCount": "{count, plural, zero{{count} filters set} one{{count} filter set} other{{count} filters set}}", + "@savedViewsFiltersSetCount": { + "placeholders": { + "count": {} + } + }, "savedViewShowInSidebarLabel": "Show in sidebar", "@savedViewShowInSidebarLabel": {}, "savedViewShowOnDashboardLabel": "Show on dashboard", "@savedViewShowOnDashboardLabel": {}, - "savedViewsLabel": "Saved Views", + "savedViewsLabel": "Views", "@savedViewsLabel": {}, + "scannerPageClearAllLabel": "Clear all", + "@scannerPageClearAllLabel": {}, "scannerPageImagePreviewTitle": "Scan", "@scannerPageImagePreviewTitle": {}, + "scannerPagePreviewLabel": "Preview", + "@scannerPagePreviewLabel": {}, + "scannerPageUploadLabel": "Upload", + "@scannerPageUploadLabel": {}, "serverInformationPaperlessVersionText": "Paperless server version", "@serverInformationPaperlessVersionText": {}, "settingsPageAppearanceSettingDarkThemeLabel": "Dark Theme", @@ -627,6 +647,5 @@ "verifyIdentityPageTitle": "Verify your identity", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", - "@verifyIdentityPageVerifyIdentityButtonLabel": {}, - "genericActionCloseLabel": "Close" + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb new file mode 100644 index 0000000..d8e6a7f --- /dev/null +++ b/lib/l10n/intl_pl.arb @@ -0,0 +1,651 @@ +{ + "@@locale": "pl", + "aboutDialogDevelopedByText": "Developed by {name}", + "@aboutDialogDevelopedByText": { + "placeholders": { + "name": {} + } + }, + "accountSettingsTitle": "Account", + "@accountSettingsTitle": {}, + "addCorrespondentPageTitle": "New Correspondent", + "@addCorrespondentPageTitle": {}, + "addDocumentTypePageTitle": "Nowy rodzaj dokumentu", + "@addDocumentTypePageTitle": {}, + "addStoragePathPageTitle": "New Storage Path", + "@addStoragePathPageTitle": {}, + "addTagPageTitle": "Nowy tag", + "@addTagPageTitle": {}, + "appDrawerAboutInfoLoadingText": "Retrieving application information...", + "@appDrawerAboutInfoLoadingText": {}, + "appDrawerAboutLabel": "O aplikacji", + "@appDrawerAboutLabel": {}, + "appDrawerHeaderLoggedInAsText": "Zalogowano jako", + "@appDrawerHeaderLoggedInAsText": {}, + "appDrawerLogoutLabel": "Disconnect", + "@appDrawerLogoutLabel": {}, + "appDrawerReportBugLabel": "Report a Bug", + "@appDrawerReportBugLabel": {}, + "appDrawerSettingsLabel": "Ustawienia", + "@appDrawerSettingsLabel": {}, + "appSettingsBiometricAuthenticationDescriptionText": "Authenticate on app start", + "@appSettingsBiometricAuthenticationDescriptionText": {}, + "appSettingsBiometricAuthenticationLabel": "Biometric authentication", + "@appSettingsBiometricAuthenticationLabel": {}, + "appSettingsDisableBiometricAuthenticationReasonText": "Authenticate to disable biometric authentication", + "@appSettingsDisableBiometricAuthenticationReasonText": {}, + "appSettingsEnableBiometricAuthenticationReasonText": "Authenticate to enable biometric authentication", + "@appSettingsEnableBiometricAuthenticationReasonText": {}, + "appTitleText": "Paperless Mobile", + "@appTitleText": {}, + "bottomNavDocumentsPageLabel": "Documents", + "@bottomNavDocumentsPageLabel": {}, + "bottomNavInboxPageLabel": "Skrzynka odbiorcza", + "@bottomNavInboxPageLabel": {}, + "bottomNavLabelsPageLabel": "Labels", + "@bottomNavLabelsPageLabel": {}, + "bottomNavScannerPageLabel": "Scanner", + "@bottomNavScannerPageLabel": {}, + "colorSchemeOptionClassic": "Classic", + "@colorSchemeOptionClassic": {}, + "colorSchemeOptionDynamic": "Dynamic", + "@colorSchemeOptionDynamic": {}, + "correspondentFormFieldSearchHintText": "Zacznij pisać...", + "@correspondentFormFieldSearchHintText": {}, + "deleteViewDialogContentText": "Do you really want to delete this view?", + "@deleteViewDialogContentText": {}, + "deleteViewDialogTitleText": "Delete view ", + "@deleteViewDialogTitleText": {}, + "documentAddedPropertyLabel": "Added at", + "@documentAddedPropertyLabel": {}, + "documentArchiveSerialNumberPropertyLongLabel": "Numer Seryjny Archiwum", + "@documentArchiveSerialNumberPropertyLongLabel": {}, + "documentArchiveSerialNumberPropertyShortLabel": "ASN", + "@documentArchiveSerialNumberPropertyShortLabel": {}, + "documentCorrespondentPropertyLabel": "Correspondent", + "@documentCorrespondentPropertyLabel": {}, + "documentCreatedPropertyLabel": "Created at", + "@documentCreatedPropertyLabel": {}, + "documentDeleteSuccessMessage": "Dokument pomyślnie usunięty.", + "@documentDeleteSuccessMessage": {}, + "documentDetailsPageAssignAsnButtonLabel": "Assign", + "@documentDetailsPageAssignAsnButtonLabel": {}, + "documentDetailsPageDeleteTooltip": "Usuń", + "@documentDetailsPageDeleteTooltip": {}, + "documentDetailsPageDownloadTooltip": "Pobierz", + "@documentDetailsPageDownloadTooltip": {}, + "documentDetailsPageEditTooltip": "Edytuj", + "@documentDetailsPageEditTooltip": {}, + "documentDetailsPageLoadFullContentLabel": "Load full content", + "@documentDetailsPageLoadFullContentLabel": {}, + "documentDetailsPageNoPdfViewerFoundErrorMessage": "Nie znaleziono aplikacji do wyświetlania plików PDF", + "@documentDetailsPageNoPdfViewerFoundErrorMessage": {}, + "documentDetailsPageOpenInSystemViewerTooltip": "Otwórz w przeglądarce systemowej", + "@documentDetailsPageOpenInSystemViewerTooltip": {}, + "documentDetailsPageOpenPdfPermissionDeniedErrorMessage": "Nie można otworzyć pliku: ", + "@documentDetailsPageOpenPdfPermissionDeniedErrorMessage": {}, + "documentDetailsPagePreviewTooltip": "Podgląd", + "@documentDetailsPagePreviewTooltip": {}, + "documentDetailsPageShareTooltip": "Udostępnij", + "@documentDetailsPageShareTooltip": {}, + "documentDetailsPageSimilarDocumentsLabel": "Podobne Dokumenty", + "@documentDetailsPageSimilarDocumentsLabel": {}, + "documentDetailsPageTabContentLabel": "Treść", + "@documentDetailsPageTabContentLabel": {}, + "documentDetailsPageTabMetaDataLabel": "Meta dane", + "@documentDetailsPageTabMetaDataLabel": {}, + "documentDetailsPageTabOverviewLabel": "Przegląd", + "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Podobne Dokumenty", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, + "documentDocumentTypePropertyLabel": "Rodzaj dokumentu", + "@documentDocumentTypePropertyLabel": {}, + "documentDownloadSuccessMessage": "Document successfully downloaded.", + "@documentDownloadSuccessMessage": {}, + "documentEditPageSuggestionsLabel": "Suggestions: ", + "@documentEditPageSuggestionsLabel": {}, + "documentEditPageTitle": "Edytuj Dokument", + "@documentEditPageTitle": {}, + "documentFilterAdvancedLabel": "Advanced", + "@documentFilterAdvancedLabel": {}, + "documentFilterApplyFilterLabel": "Apply", + "@documentFilterApplyFilterLabel": {}, + "documentFilterQueryOptionsAsnLabel": "ASN", + "@documentFilterQueryOptionsAsnLabel": {}, + "documentFilterQueryOptionsExtendedLabel": "Extended", + "@documentFilterQueryOptionsExtendedLabel": {}, + "documentFilterQueryOptionsTitleAndContentLabel": "Tytuł i treść", + "@documentFilterQueryOptionsTitleAndContentLabel": {}, + "documentFilterQueryOptionsTitleLabel": "Tytuł", + "@documentFilterQueryOptionsTitleLabel": {}, + "documentFilterResetLabel": "Reset", + "@documentFilterResetLabel": {}, + "documentFilterSearchLabel": "Szukaj", + "@documentFilterSearchLabel": {}, + "documentFilterTitle": "Filter Documents", + "@documentFilterTitle": {}, + "documentMetaDataChecksumLabel": "Original MD5-Checksum", + "@documentMetaDataChecksumLabel": {}, + "documentMetaDataMediaFilenamePropertyLabel": "Media Filename", + "@documentMetaDataMediaFilenamePropertyLabel": {}, + "documentMetaDataOriginalFileSizeLabel": "Original File Size", + "@documentMetaDataOriginalFileSizeLabel": {}, + "documentMetaDataOriginalMimeTypeLabel": "Original MIME-Type", + "@documentMetaDataOriginalMimeTypeLabel": {}, + "documentModifiedPropertyLabel": "Modified at", + "@documentModifiedPropertyLabel": {}, + "documentPreviewPageTitle": "Podgląd", + "@documentPreviewPageTitle": {}, + "documentScannerPageAddScanButtonLabel": "Zeskanuj dokument", + "@documentScannerPageAddScanButtonLabel": {}, + "documentScannerPageEmptyStateText": "No documents scanned yet.", + "@documentScannerPageEmptyStateText": {}, + "documentScannerPageOrText": "lub", + "@documentScannerPageOrText": {}, + "documentScannerPageResetButtonTooltipText": "Delete all scans", + "@documentScannerPageResetButtonTooltipText": {}, + "documentScannerPageTitle": "Skanuj", + "@documentScannerPageTitle": {}, + "documentScannerPageUploadButtonTooltip": "Prześlij dokument z tego urządzenia", + "@documentScannerPageUploadButtonTooltip": {}, + "documentScannerPageUploadFromThisDeviceButtonLabel": "Upload a document from this device", + "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, + "documentSearchHistory": "Historia", + "@documentSearchHistory": {}, + "documentSearchNoMatchesFound": "No matches found.", + "@documentSearchNoMatchesFound": {}, + "documentSearchPageRemoveFromHistory": "Usunąć z historii wyszukiwania?", + "@documentSearchPageRemoveFromHistory": {}, + "documentSearchResults": "Wyniki", + "@documentSearchResults": {}, + "documentSearchSearchDocuments": "Search documents", + "@documentSearchSearchDocuments": {}, + "documentsEmptyStateResetFilterLabel": "Reset filter", + "@documentsEmptyStateResetFilterLabel": {}, + "documentsFilterPageAdvancedLabel": "Advanced", + "@documentsFilterPageAdvancedLabel": {}, + "documentsFilterPageApplyFilterLabel": "Apply", + "@documentsFilterPageApplyFilterLabel": {}, + "documentsFilterPageDateRangeLastMonthLabel": "Last Month", + "@documentsFilterPageDateRangeLastMonthLabel": {}, + "documentsFilterPageDateRangeLastSevenDaysLabel": "Last 7 Days", + "@documentsFilterPageDateRangeLastSevenDaysLabel": {}, + "documentsFilterPageDateRangeLastThreeMonthsLabel": "Last 3 Months", + "@documentsFilterPageDateRangeLastThreeMonthsLabel": {}, + "documentsFilterPageDateRangeLastYearLabel": "Last Year", + "@documentsFilterPageDateRangeLastYearLabel": {}, + "documentsFilterPageQueryOptionsAsnLabel": "ASN", + "@documentsFilterPageQueryOptionsAsnLabel": {}, + "documentsFilterPageQueryOptionsExtendedLabel": "Extended", + "@documentsFilterPageQueryOptionsExtendedLabel": {}, + "documentsFilterPageQueryOptionsTitleAndContentLabel": "Title & Content", + "@documentsFilterPageQueryOptionsTitleAndContentLabel": {}, + "documentsFilterPageQueryOptionsTitleLabel": "Title", + "@documentsFilterPageQueryOptionsTitleLabel": {}, + "documentsFilterPageSearchLabel": "Szukaj", + "@documentsFilterPageSearchLabel": {}, + "documentsFilterPageTitle": "Filter Documents", + "@documentsFilterPageTitle": {}, + "documentsPageBulkDeleteSuccessfulText": "Dokument pomyślnie usunięty.", + "@documentsPageBulkDeleteSuccessfulText": {}, + "documentsPageEmptyStateNothingHereText": "There seems to be nothing here...", + "@documentsPageEmptyStateNothingHereText": {}, + "documentsPageEmptyStateOopsText": "Ups.", + "@documentsPageEmptyStateOopsText": {}, + "documentsPageNewDocumentAvailableText": "New document available!", + "@documentsPageNewDocumentAvailableText": {}, + "documentsPageOrderByLabel": "Order By", + "@documentsPageOrderByLabel": {}, + "documentsPageSelectionBulkDeleteDialogContinueText": "This action is irreversible. Do you wish to proceed anyway?", + "@documentsPageSelectionBulkDeleteDialogContinueText": {}, + "documentsPageSelectionBulkDeleteDialogTitle": "Potwierdź usunięcie", + "@documentsPageSelectionBulkDeleteDialogTitle": {}, + "documentsPageSelectionBulkDeleteDialogWarningTextMany": "Are you sure you want to delete the following documents?", + "@documentsPageSelectionBulkDeleteDialogWarningTextMany": {}, + "documentsPageSelectionBulkDeleteDialogWarningTextOne": "Are you sure you want to delete the following document?", + "@documentsPageSelectionBulkDeleteDialogWarningTextOne": {}, + "documentsPageTitle": "Documents", + "@documentsPageTitle": {}, + "documentsSelectedText": "selected", + "@documentsSelectedText": {}, + "documentStoragePathPropertyLabel": "Storage Path", + "@documentStoragePathPropertyLabel": {}, + "documentsUploadPageTitle": "Prepare document", + "@documentsUploadPageTitle": {}, + "documentTagsPropertyLabel": "Tagi", + "@documentTagsPropertyLabel": {}, + "documentTitlePropertyLabel": "Tytuł", + "@documentTitlePropertyLabel": {}, + "documentTypeFormFieldSearchHintText": "Zacznij pisać...", + "@documentTypeFormFieldSearchHintText": {}, + "documentUpdateSuccessMessage": "Dokument został pomyślnie zaktualizowany ", + "@documentUpdateSuccessMessage": {}, + "documentUploadFileNameLabel": "Nazwa Pliku", + "@documentUploadFileNameLabel": {}, + "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronize title and filename", + "@documentUploadPageSynchronizeTitleAndFilenameLabel": {}, + "documentUploadProcessingSuccessfulReloadActionText": "Reload", + "@documentUploadProcessingSuccessfulReloadActionText": {}, + "documentUploadProcessingSuccessfulText": "Dokument pomyślnie przetworzony.", + "@documentUploadProcessingSuccessfulText": {}, + "documentUploadSuccessText": "Dokument pomyślnie przesłany, przetwarzam...", + "@documentUploadSuccessText": {}, + "editLabelPageConfirmDeletionDialogTitle": "Potwierdź usunięcie", + "@editLabelPageConfirmDeletionDialogTitle": {}, + "editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?", + "@editLabelPageDeletionDialogText": {}, + "errorMessageAcknowledgeTasksError": "Could not acknowledge tasks.", + "@errorMessageAcknowledgeTasksError": {}, + "errorMessageAuthenticationFailed": "Authentication failed, please try again.", + "@errorMessageAuthenticationFailed": {}, + "errorMessageAutocompleteQueryError": "An error ocurred while trying to autocomplete your query.", + "@errorMessageAutocompleteQueryError": {}, + "errorMessageBiometricAuthenticationFailed": "Biometric authentication failed.", + "@errorMessageBiometricAuthenticationFailed": {}, + "errorMessageBiotmetricsNotSupported": "Biometric authentication not supported on this device.", + "@errorMessageBiotmetricsNotSupported": {}, + "errorMessageBulkActionFailed": "Could not bulk edit documents.", + "@errorMessageBulkActionFailed": {}, + "errorMessageCorrespondentCreateFailed": "Could not create correspondent, please try again.", + "@errorMessageCorrespondentCreateFailed": {}, + "errorMessageCorrespondentLoadFailed": "Could not load correspondents.", + "@errorMessageCorrespondentLoadFailed": {}, + "errorMessageCreateSavedViewError": "Could not create saved view, please try again.", + "@errorMessageCreateSavedViewError": {}, + "errorMessageDeleteSavedViewError": "Could not delete saved view, please try again", + "@errorMessageDeleteSavedViewError": {}, + "errorMessageDeviceOffline": "You are currently offline. Please make sure you are connected to the internet.", + "@errorMessageDeviceOffline": {}, + "errorMessageDocumentAsnQueryFailed": "Could not assign archive serial number.", + "@errorMessageDocumentAsnQueryFailed": {}, + "errorMessageDocumentDeleteFailed": "Could not delete document, please try again.", + "@errorMessageDocumentDeleteFailed": {}, + "errorMessageDocumentLoadFailed": "Could not load documents, please try again.", + "@errorMessageDocumentLoadFailed": {}, + "errorMessageDocumentPreviewFailed": "Could not load document preview.", + "@errorMessageDocumentPreviewFailed": {}, + "errorMessageDocumentTypeCreateFailed": "Could not create document, please try again.", + "@errorMessageDocumentTypeCreateFailed": {}, + "errorMessageDocumentTypeLoadFailed": "Could not load document types, please try again.", + "@errorMessageDocumentTypeLoadFailed": {}, + "errorMessageDocumentUpdateFailed": "Could not update document, please try again.", + "@errorMessageDocumentUpdateFailed": {}, + "errorMessageDocumentUploadFailed": "Could not upload document, please try again.", + "@errorMessageDocumentUploadFailed": {}, + "errorMessageInvalidClientCertificateConfiguration": "Invalid certificate or missing passphrase, please try again", + "@errorMessageInvalidClientCertificateConfiguration": {}, + "errorMessageLoadSavedViewsError": "Could not load views.", + "@errorMessageLoadSavedViewsError": {}, + "errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate.", + "@errorMessageMissingClientCertificate": {}, + "errorMessageNotAuthenticated": "User is not authenticated.", + "@errorMessageNotAuthenticated": {}, + "errorMessageRequestTimedOut": "The request to the server timed out.", + "@errorMessageRequestTimedOut": {}, + "errorMessageScanRemoveFailed": "An error occurred removing the scans.", + "@errorMessageScanRemoveFailed": {}, + "errorMessageServerUnreachable": "Could not reach your Paperless server, is it up and running?", + "@errorMessageServerUnreachable": {}, + "errorMessageSimilarQueryError": "Could not load similar documents.", + "@errorMessageSimilarQueryError": {}, + "errorMessageStoragePathCreateFailed": "Could not create storage path, please try again.", + "@errorMessageStoragePathCreateFailed": {}, + "errorMessageStoragePathLoadFailed": "Could not load storage paths.", + "@errorMessageStoragePathLoadFailed": {}, + "errorMessageSuggestionsQueryError": "Could not load suggestions.", + "@errorMessageSuggestionsQueryError": {}, + "errorMessageTagCreateFailed": "Could not create tag, please try again.", + "@errorMessageTagCreateFailed": {}, + "errorMessageTagLoadFailed": "Could not load tags.", + "@errorMessageTagLoadFailed": {}, + "errorMessageUnknonwnError": "An unknown error occurred.", + "@errorMessageUnknonwnError": {}, + "errorMessageUnsupportedFileFormat": "This file format is not supported.", + "@errorMessageUnsupportedFileFormat": {}, + "errorReportLabel": "REPORT", + "@errorReportLabel": {}, + "extendedDateRangeDialogAbsoluteLabel": "Absolute", + "@extendedDateRangeDialogAbsoluteLabel": {}, + "extendedDateRangeDialogHintText": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.", + "@extendedDateRangeDialogHintText": {}, + "extendedDateRangeDialogRelativeAmountLabel": "Amount", + "@extendedDateRangeDialogRelativeAmountLabel": {}, + "extendedDateRangeDialogRelativeLabel": "Relative", + "@extendedDateRangeDialogRelativeLabel": {}, + "extendedDateRangeDialogRelativeLastLabel": "Last", + "@extendedDateRangeDialogRelativeLastLabel": {}, + "extendedDateRangeDialogRelativeTimeUnitLabel": "Time unit", + "@extendedDateRangeDialogRelativeTimeUnitLabel": {}, + "extendedDateRangeDialogTitle": "Wybierz zakres dat", + "@extendedDateRangeDialogTitle": {}, + "extendedDateRangePickerAfterLabel": "Po", + "@extendedDateRangePickerAfterLabel": {}, + "extendedDateRangePickerBeforeLabel": "Przed", + "@extendedDateRangePickerBeforeLabel": {}, + "extendedDateRangePickerDayText": "{count, plural, zero{days} one{day} other{days}}", + "@extendedDateRangePickerDayText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastDaysLabel": "{count, plural, zero{} one{Yesterday} other{Last {count} days}}", + "@extendedDateRangePickerLastDaysLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastMonthsLabel": "{count, plural, zero{} one{Last month} other{Last {count} months}}", + "@extendedDateRangePickerLastMonthsLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastText": "Last", + "@extendedDateRangePickerLastText": {}, + "extendedDateRangePickerLastWeeksLabel": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}", + "@extendedDateRangePickerLastWeeksLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastYearsLabel": "{count, plural, zero{} one{Last year} other{Last {count} years}}", + "@extendedDateRangePickerLastYearsLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerMonthText": "{count, plural, zero{} one{month} other{months}}", + "@extendedDateRangePickerMonthText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerWeekText": "{count, plural, zero{} one{week} other{weeks}}", + "@extendedDateRangePickerWeekText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerYearText": "{count, plural, zero{} one{year} other{years}}", + "@extendedDateRangePickerYearText": { + "placeholders": { + "count": {} + } + }, + "genericAcknowledgeLabel": "Got it!", + "@genericAcknowledgeLabel": {}, + "genericActionCancelLabel": "Cancel", + "@genericActionCancelLabel": {}, + "genericActionCloseLabel": "Close", + "@genericActionCloseLabel": {}, + "genericActionCreateLabel": "Create", + "@genericActionCreateLabel": {}, + "genericActionDeleteLabel": "Delete", + "@genericActionDeleteLabel": {}, + "genericActionEditLabel": "Edit", + "@genericActionEditLabel": {}, + "genericActionOkLabel": "Ok", + "@genericActionOkLabel": {}, + "genericActionSaveLabel": "Save", + "@genericActionSaveLabel": {}, + "genericActionSelectText": "Select", + "@genericActionSelectText": {}, + "genericActionUpdateLabel": "Zapisz zmiany", + "@genericActionUpdateLabel": {}, + "genericActionUploadLabel": "Upload", + "@genericActionUploadLabel": {}, + "genericMessageOfflineText": "Jesteście w trybie offline.", + "@genericMessageOfflineText": {}, + "inboxPageAssignAsnLabel": "Assign ASN", + "@inboxPageAssignAsnLabel": {}, + "inboxPageDocumentRemovedMessageText": "Dokument usunięty ze skrzynki odbiorczej", + "@inboxPageDocumentRemovedMessageText": {}, + "inboxPageMarkAllAsSeenConfirmationDialogText": "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?", + "@inboxPageMarkAllAsSeenConfirmationDialogText": {}, + "inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Mark all as seen?", + "@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {}, + "inboxPageMarkAllAsSeenLabel": "All seen", + "@inboxPageMarkAllAsSeenLabel": {}, + "inboxPageMarkAsSeenText": "Mark as seen", + "@inboxPageMarkAsSeenText": {}, + "inboxPageNoNewDocumentsRefreshLabel": "Odświerz", + "@inboxPageNoNewDocumentsRefreshLabel": {}, + "inboxPageNoNewDocumentsText": "You do not have unseen documents.", + "@inboxPageNoNewDocumentsText": {}, + "inboxPageQuickActionsLabel": "Quick Action", + "@inboxPageQuickActionsLabel": {}, + "inboxPageSuggestionSuccessfullyAppliedMessage": "Suggestion successfully applied.", + "@inboxPageSuggestionSuccessfullyAppliedMessage": {}, + "inboxPageTodayText": "Dzisiaj", + "@inboxPageTodayText": {}, + "inboxPageUndoRemoveText": "Cofnij", + "@inboxPageUndoRemoveText": {}, + "inboxPageUnseenText": "unseen", + "@inboxPageUnseenText": {}, + "inboxPageUsageHintText": "Hint: Swipe left to mark a document as seen and remove all inbox tags from the document.", + "@inboxPageUsageHintText": {}, + "inboxPageYesterdayText": "Wczoraj", + "@inboxPageYesterdayText": {}, + "labelAnyAssignedText": "Any assigned", + "@labelAnyAssignedText": {}, + "labelFormFieldNoItemsFoundText": "No items found!", + "@labelFormFieldNoItemsFoundText": {}, + "labelIsInsensivitePropertyLabel": "Case Irrelevant", + "@labelIsInsensivitePropertyLabel": {}, + "labelMatchingAlgorithmPropertyLabel": "Matching Algorithm", + "@labelMatchingAlgorithmPropertyLabel": {}, + "labelMatchPropertyLabel": "Match", + "@labelMatchPropertyLabel": {}, + "labelNamePropertyLabel": "Nazwa", + "@labelNamePropertyLabel": {}, + "labelNotAssignedText": "Not assigned", + "@labelNotAssignedText": {}, + "labelsPageCorrespondentEmptyStateAddNewLabel": "Add new correspondent", + "@labelsPageCorrespondentEmptyStateAddNewLabel": {}, + "labelsPageCorrespondentEmptyStateDescriptionText": "You don't seem to have any correspondents set up.", + "@labelsPageCorrespondentEmptyStateDescriptionText": {}, + "labelsPageCorrespondentsTitleText": "Correspondents", + "@labelsPageCorrespondentsTitleText": {}, + "labelsPageDocumentTypeEmptyStateAddNewLabel": "Dodaj nowy rodzaj dokumentu", + "@labelsPageDocumentTypeEmptyStateAddNewLabel": {}, + "labelsPageDocumentTypeEmptyStateDescriptionText": "You don't seem to have any document types set up.", + "@labelsPageDocumentTypeEmptyStateDescriptionText": {}, + "labelsPageDocumentTypesTitleText": "Rodzaje dokumentów", + "@labelsPageDocumentTypesTitleText": {}, + "labelsPageStoragePathEmptyStateAddNewLabel": "Add new storage path", + "@labelsPageStoragePathEmptyStateAddNewLabel": {}, + "labelsPageStoragePathEmptyStateDescriptionText": "You don't seem to have any storage paths set up.", + "@labelsPageStoragePathEmptyStateDescriptionText": {}, + "labelsPageStoragePathTitleText": "Storage Paths", + "@labelsPageStoragePathTitleText": {}, + "labelsPageTagsEmptyStateAddNewLabel": "Dodaj nowy tag", + "@labelsPageTagsEmptyStateAddNewLabel": {}, + "labelsPageTagsEmptyStateDescriptionText": "You don't seem to have any tags set up.", + "@labelsPageTagsEmptyStateDescriptionText": {}, + "labelsPageTagsTitleText": "Tagi", + "@labelsPageTagsTitleText": {}, + "linkedDocumentsPageTitle": "Linked Documents", + "@linkedDocumentsPageTitle": {}, + "loginPageAdvancedLabel": "Advanced Settings", + "@loginPageAdvancedLabel": {}, + "loginPageClientCertificatePassphraseLabel": "Passphrase", + "@loginPageClientCertificatePassphraseLabel": {}, + "loginPageClientCertificateSettingDescriptionText": "Configure Mutual TLS Authentication", + "@loginPageClientCertificateSettingDescriptionText": {}, + "loginPageClientCertificateSettingInvalidFileFormatValidationText": "Invalid certificate format, only .pfx is allowed", + "@loginPageClientCertificateSettingInvalidFileFormatValidationText": {}, + "loginPageClientCertificateSettingLabel": "Client Certificate", + "@loginPageClientCertificateSettingLabel": {}, + "loginPageClientCertificateSettingSelectFileText": "Select file...", + "@loginPageClientCertificateSettingSelectFileText": {}, + "loginPageContinueLabel": "Kontynuuj", + "@loginPageContinueLabel": {}, + "loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Incorrect or missing certificate passphrase.", + "@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {}, + "loginPageLoginButtonLabel": "Polącz", + "@loginPageLoginButtonLabel": {}, + "loginPagePasswordFieldLabel": "Hasło", + "@loginPagePasswordFieldLabel": {}, + "loginPagePasswordValidatorMessageText": "Hasło nie może być puste.", + "@loginPagePasswordValidatorMessageText": {}, + "loginPageReachabilityConnectionTimeoutText": "Connection timed out.", + "@loginPageReachabilityConnectionTimeoutText": {}, + "loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.", + "@loginPageReachabilityInvalidClientCertificateConfigurationText": {}, + "loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.", + "@loginPageReachabilityMissingClientCertificateText": {}, + "loginPageReachabilityNotReachableText": "Could not establish a connection to the server.", + "@loginPageReachabilityNotReachableText": {}, + "loginPageReachabilitySuccessText": "Connection successfully established.", + "@loginPageReachabilitySuccessText": {}, + "loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address and your internet connection. ", + "@loginPageReachabilityUnresolvedHostText": {}, + "loginPageServerUrlFieldLabel": "Adres serwera", + "@loginPageServerUrlFieldLabel": {}, + "loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.", + "@loginPageServerUrlValidatorMessageInvalidAddressText": {}, + "loginPageServerUrlValidatorMessageMissingSchemeText": "Server address must include a scheme.", + "@loginPageServerUrlValidatorMessageMissingSchemeText": {}, + "loginPageServerUrlValidatorMessageRequiredText": "Server address must not be empty.", + "@loginPageServerUrlValidatorMessageRequiredText": {}, + "loginPageSignInButtonLabel": "Sign In", + "@loginPageSignInButtonLabel": {}, + "loginPageSignInTitle": "Sign In", + "@loginPageSignInTitle": {}, + "loginPageSignInToPrefixText": "Sign in to {serverAddress}", + "@loginPageSignInToPrefixText": { + "placeholders": { + "serverAddress": {} + } + }, + "loginPageTitle": "Connect to Paperless", + "@loginPageTitle": {}, + "loginPageUsernameLabel": "Username", + "@loginPageUsernameLabel": {}, + "loginPageUsernameValidatorMessageText": "Username must not be empty.", + "@loginPageUsernameValidatorMessageText": {}, + "matchingAlgorithmAllDescription": "Document contains all of these words", + "@matchingAlgorithmAllDescription": {}, + "matchingAlgorithmAllName": "All", + "@matchingAlgorithmAllName": {}, + "matchingAlgorithmAnyDescription": "Document contains any of these words", + "@matchingAlgorithmAnyDescription": {}, + "matchingAlgorithmAnyName": "Any", + "@matchingAlgorithmAnyName": {}, + "matchingAlgorithmAutoDescription": "Learn matching automatically", + "@matchingAlgorithmAutoDescription": {}, + "matchingAlgorithmAutoName": "Auto", + "@matchingAlgorithmAutoName": {}, + "matchingAlgorithmExactDescription": "Document contains this string", + "@matchingAlgorithmExactDescription": {}, + "matchingAlgorithmExactName": "Exact", + "@matchingAlgorithmExactName": {}, + "matchingAlgorithmFuzzyDescription": "Document contains a word similar to this word", + "@matchingAlgorithmFuzzyDescription": {}, + "matchingAlgorithmFuzzyName": "Fuzzy", + "@matchingAlgorithmFuzzyName": {}, + "matchingAlgorithmRegexDescription": "Document matches this regular expression", + "@matchingAlgorithmRegexDescription": {}, + "matchingAlgorithmRegexName": "Regular Expression", + "@matchingAlgorithmRegexName": {}, + "offlineWidgetText": "Nie można było nawiązać połączenia internetowego.", + "@offlineWidgetText": {}, + "onboardingDoneButtonLabel": "Done", + "@onboardingDoneButtonLabel": {}, + "onboardingNextButtonLabel": "Następne", + "@onboardingNextButtonLabel": {}, + "receiveSharedFilePermissionDeniedMessage": "Could not access the received file. Please try to open the app before sharing.", + "@receiveSharedFilePermissionDeniedMessage": {}, + "referencedDocumentsReadOnlyHintText": "This is a read-only view! You cannot edit or remove documents. A maximum of 100 referenced documents will be loaded.", + "@referencedDocumentsReadOnlyHintText": {}, + "savedViewCreateNewLabel": "New View", + "@savedViewCreateNewLabel": {}, + "savedViewCreateTooltipText": "Creates a new view based on the current filter criteria.", + "@savedViewCreateTooltipText": {}, + "savedViewNameLabel": "Nazwa", + "@savedViewNameLabel": {}, + "savedViewsEmptyStateText": "Create views to quickly filter your documents.", + "@savedViewsEmptyStateText": {}, + "savedViewsFiltersSetCount": "{count, plural, zero{{count} filters set} one{{count} filter set} other{{count} filters set}}", + "@savedViewsFiltersSetCount": { + "placeholders": { + "count": {} + } + }, + "savedViewShowInSidebarLabel": "Show in sidebar", + "@savedViewShowInSidebarLabel": {}, + "savedViewShowOnDashboardLabel": "Show on dashboard", + "@savedViewShowOnDashboardLabel": {}, + "savedViewsLabel": "Views", + "@savedViewsLabel": {}, + "scannerPageClearAllLabel": "Clear all", + "@scannerPageClearAllLabel": {}, + "scannerPageImagePreviewTitle": "Skanuj", + "@scannerPageImagePreviewTitle": {}, + "scannerPagePreviewLabel": "Preview", + "@scannerPagePreviewLabel": {}, + "scannerPageUploadLabel": "Upload", + "@scannerPageUploadLabel": {}, + "serverInformationPaperlessVersionText": "Wersja serwera Paperless", + "@serverInformationPaperlessVersionText": {}, + "settingsPageAppearanceSettingDarkThemeLabel": "Motyw ciemny", + "@settingsPageAppearanceSettingDarkThemeLabel": {}, + "settingsPageAppearanceSettingLightThemeLabel": "Motyw jasny", + "@settingsPageAppearanceSettingLightThemeLabel": {}, + "settingsPageAppearanceSettingSystemThemeLabel": "Użyj motywu systemu", + "@settingsPageAppearanceSettingSystemThemeLabel": {}, + "settingsPageAppearanceSettingTitle": "Wygląd", + "@settingsPageAppearanceSettingTitle": {}, + "settingsPageApplicationSettingsDescriptionText": "Język i wygląd", + "@settingsPageApplicationSettingsDescriptionText": {}, + "settingsPageApplicationSettingsLabel": "Aplikacja", + "@settingsPageApplicationSettingsLabel": {}, + "settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.", + "@settingsPageColorSchemeSettingDialogDescription": {}, + "settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "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.", + "@settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": {}, + "settingsPageColorSchemeSettingLabel": "Kolory", + "@settingsPageColorSchemeSettingLabel": {}, + "settingsPageLanguageSettingLabel": "Język", + "@settingsPageLanguageSettingLabel": {}, + "settingsPageSecuritySettingsDescriptionText": "Uwierzytelnianie biometryczne", + "@settingsPageSecuritySettingsDescriptionText": {}, + "settingsPageSecuritySettingsLabel": "Zabezpieczenia", + "@settingsPageSecuritySettingsLabel": {}, + "settingsPageStorageSettingsDescriptionText": "Manage files and storage space", + "@settingsPageStorageSettingsDescriptionText": {}, + "settingsPageStorageSettingsLabel": "Storage", + "@settingsPageStorageSettingsLabel": {}, + "settingsThemeModeDarkLabel": "Ciemny", + "@settingsThemeModeDarkLabel": {}, + "settingsThemeModeLightLabel": "Jasny", + "@settingsThemeModeLightLabel": {}, + "settingsThemeModeSystemLabel": "System", + "@settingsThemeModeSystemLabel": {}, + "sortDocumentAscending": "Ascending", + "@sortDocumentAscending": {}, + "sortDocumentDescending": "Descending", + "@sortDocumentDescending": {}, + "storagePathParameterDayLabel": "dzień", + "@storagePathParameterDayLabel": {}, + "storagePathParameterMonthLabel": "miesiąc", + "@storagePathParameterMonthLabel": {}, + "storagePathParameterYearLabel": "rok", + "@storagePathParameterYearLabel": {}, + "tagColorPropertyLabel": "Kolor", + "@tagColorPropertyLabel": {}, + "tagFormFieldSearchHintText": "Filter tags...", + "@tagFormFieldSearchHintText": {}, + "tagInboxTagPropertyLabel": "Tag skrzynki odbiorczej", + "@tagInboxTagPropertyLabel": {}, + "uploadPageAutomaticallInferredFieldsHintText": "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.", + "@uploadPageAutomaticallInferredFieldsHintText": {}, + "verifyIdentityPageDescriptionText": "Use the configured biometric factor to authenticate and unlock your documents.", + "@verifyIdentityPageDescriptionText": {}, + "verifyIdentityPageLogoutButtonLabel": "Disconnect", + "@verifyIdentityPageLogoutButtonLabel": {}, + "verifyIdentityPageTitle": "Verify your identity", + "@verifyIdentityPageTitle": {}, + "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", + "@verifyIdentityPageVerifyIdentityButtonLabel": {} +} \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index f9a6e61..3451880 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsTitle": "Account", + "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Yeni ek yazar", "@addCorrespondentPageTitle": {}, "addDocumentTypePageTitle": "Yeni Belge Türü", @@ -150,10 +152,14 @@ "@documentScannerPageUploadFromThisDeviceButtonLabel": {}, "documentSearchHistory": "History", "@documentSearchHistory": {}, + "documentSearchNoMatchesFound": "No matches found.", + "@documentSearchNoMatchesFound": {}, "documentSearchPageRemoveFromHistory": "Remove from search history?", "@documentSearchPageRemoveFromHistory": {}, "documentSearchResults": "Results", "@documentSearchResults": {}, + "documentSearchSearchDocuments": "Search documents", + "@documentSearchSearchDocuments": {}, "documentsEmptyStateResetFilterLabel": "Filtreyi sıfırla", "@documentsEmptyStateResetFilterLabel": {}, "documentsFilterPageAdvancedLabel": "Gelişmiş", @@ -370,6 +376,8 @@ "@genericAcknowledgeLabel": {}, "genericActionCancelLabel": "İptal", "@genericActionCancelLabel": {}, + "genericActionCloseLabel": "Close", + "@genericActionCloseLabel": {}, "genericActionCreateLabel": "Yarat", "@genericActionCreateLabel": {}, "genericActionDeleteLabel": "Sil", @@ -558,14 +566,26 @@ "@savedViewNameLabel": {}, "savedViewsEmptyStateText": "Belgelerinizi hızla filtrelemek için görünümler oluşturun.", "@savedViewsEmptyStateText": {}, + "savedViewsFiltersSetCount": "{count, plural, zero{{count} filters set} one{{count} filter set} other{{count} filters set}}", + "@savedViewsFiltersSetCount": { + "placeholders": { + "count": {} + } + }, "savedViewShowInSidebarLabel": "Kenar çubuğunda göster", "@savedViewShowInSidebarLabel": {}, "savedViewShowOnDashboardLabel": "Kontrol panelinde göster", "@savedViewShowOnDashboardLabel": {}, "savedViewsLabel": "Kayıtlı Görünümler", "@savedViewsLabel": {}, + "scannerPageClearAllLabel": "Clear all", + "@scannerPageClearAllLabel": {}, "scannerPageImagePreviewTitle": "Tara", "@scannerPageImagePreviewTitle": {}, + "scannerPagePreviewLabel": "Preview", + "@scannerPagePreviewLabel": {}, + "scannerPageUploadLabel": "Upload", + "@scannerPageUploadLabel": {}, "serverInformationPaperlessVersionText": "Paperless sunucu versiyonu", "@serverInformationPaperlessVersionText": {}, "settingsPageAppearanceSettingDarkThemeLabel": "Koyu Tema", @@ -627,6 +647,5 @@ "verifyIdentityPageTitle": "Kimliğinizi doğrulayın", "@verifyIdentityPageTitle": {}, "verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula", - "@verifyIdentityPageVerifyIdentityButtonLabel": {}, - "genericActionCloseLabel": "Close" + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/theme.dart b/lib/theme.dart index 27ec7cb..adee91d 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -46,5 +46,10 @@ ThemeData buildTheme({ appBarTheme: AppBarTheme( scrolledUnderElevation: 0, ), + chipTheme: ChipThemeData( + backgroundColor: colorScheme.surfaceVariant, + checkmarkColor: colorScheme.onSurfaceVariant, + deleteIconColor: colorScheme.onSurfaceVariant, + ), ); } diff --git a/packages/paperless_api/lib/src/models/paperless_server_statistics_model.dart b/packages/paperless_api/lib/src/models/paperless_server_statistics_model.dart index d5ac901..77cd188 100644 --- a/packages/paperless_api/lib/src/models/paperless_server_statistics_model.dart +++ b/packages/paperless_api/lib/src/models/paperless_server_statistics_model.dart @@ -8,6 +8,6 @@ class PaperlessServerStatisticsModel { }); PaperlessServerStatisticsModel.fromJson(Map json) - : documentsTotal = json['documents_total'], - documentsInInbox = json['documents_inbox']; + : documentsTotal = json['documents_total'] ?? 0, + documentsInInbox = json['documents_inbox'] ?? 0; } diff --git a/pubspec.lock b/pubspec.lock index 4bb00ce..7be9f14 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -714,10 +714,10 @@ packages: dependency: "direct main" description: name: flutter_typeahead - sha256: "0ec56e1deac7556f3616f3cd53c9a25bf225dc8b72e9f44b5a7717e42bb467b5" + sha256: "73eb76fa640ea630e2d957e7b469ab2b91e4da6c4950d6032fab7009275637b7" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.3.3" flutter_web_plugins: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index d10f818..eea5a10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: mime: ^1.0.2 receive_sharing_intent: ^1.4.5 uuid: ^3.0.6 - flutter_typeahead: ^4.1.1 + flutter_typeahead: ^4.3.3 fluttertoast: ^8.1.1 paperless_api: path: packages/paperless_api From 337c178be8fbec7da9fb84440a8c50549e173b81 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 4 Feb 2023 19:24:11 +0100 Subject: [PATCH 17/25] Adds change detection mechanism for document changes --- .../notifier/document_changed_notifier.dart | 38 +++++ lib/core/type/types.dart | 3 + .../cubit/document_search_cubit.dart | 7 +- .../view/document_search_page.dart | 5 +- .../documents/bloc/documents_cubit.dart | 8 +- .../documents/view/pages/documents_page.dart | 2 + .../widgets/items/document_list_item.dart | 152 ++++++++++++------ lib/features/home/view/home_page.dart | 9 +- lib/features/inbox/bloc/inbox_cubit.dart | 10 ++ .../bloc/linked_documents_cubit.dart | 11 +- .../paged_documents_mixin.dart | 5 +- .../saved_view/view/saved_view_list.dart | 1 + lib/main.dart | 2 + .../lib/src/models/document_filter.dart | 5 + 14 files changed, 201 insertions(+), 57 deletions(-) create mode 100644 lib/core/notifier/document_changed_notifier.dart diff --git a/lib/core/notifier/document_changed_notifier.dart b/lib/core/notifier/document_changed_notifier.dart new file mode 100644 index 0000000..6597d6f --- /dev/null +++ b/lib/core/notifier/document_changed_notifier.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:paperless_api/paperless_api.dart'; +import 'package:rxdart/subjects.dart'; + +typedef DocumentChangedCallback = void Function(DocumentModel document); + +class DocumentChangedNotifier { + final Subject _updated = PublishSubject(); + final Subject _deleted = PublishSubject(); + + void notifyUpdated(DocumentModel updated) { + _updated.add(updated); + } + + void notifyDeleted(DocumentModel deleted) { + _deleted.add(deleted); + } + + List listen({ + DocumentChangedCallback? onUpdated, + DocumentChangedCallback? onDeleted, + }) { + return [ + _updated.listen((value) { + onUpdated?.call(value); + }), + _updated.listen((value) { + onDeleted?.call(value); + }), + ]; + } + + void close() { + _updated.close(); + _deleted.close(); + } +} diff --git a/lib/core/type/types.dart b/lib/core/type/types.dart index 3ed65fa..a133cbf 100644 --- a/lib/core/type/types.dart +++ b/lib/core/type/types.dart @@ -1,3 +1,6 @@ +import 'package:paperless_api/paperless_api.dart'; +import 'package:rxdart/subjects.dart'; + typedef JSON = Map; typedef PaperlessValidationErrors = Map; typedef PaperlessLocalizedErrorMessage = String; diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart index 2707926..79300f2 100644 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -8,7 +9,11 @@ class DocumentSearchCubit extends HydratedCubit with PagedDocumentsMixin { @override final PaperlessDocumentsApi api; - DocumentSearchCubit(this.api) : super(const DocumentSearchState()); + @override + final DocumentChangedNotifier notifier; + + DocumentSearchCubit(this.api, this.notifier) + : super(const DocumentSearchState()); Future search(String query) async { emit(state.copyWith( diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 71b94ad..2f0cf21 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -13,7 +13,10 @@ Future showDocumentSearchPage(BuildContext context) { return Navigator.of(context).push( MaterialPageRoute( builder: (context) => BlocProvider( - create: (context) => DocumentSearchCubit(context.read()), + create: (context) => DocumentSearchCubit( + context.read(), + context.read(), + ), child: const DocumentSearchPage(), ), ), diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 41dc04b..bc7bd04 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -12,7 +13,12 @@ class DocumentsCubit extends HydratedCubit @override final PaperlessDocumentsApi api; - DocumentsCubit(this.api) : super(const DocumentsState()); + @override + final DocumentChangedNotifier notifier; + + DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) { + reload(); + } Future bulkRemove(List documents) async { log("[DocumentsCubit] bulkRemove"); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 201f1b7..0889c9a 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:badges/badges.dart' as b; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/features/documents/view/widgets/items/document_list_item.dart b/lib/features/documents/view/widgets/items/document_list_item.dart index f63f165..e76b802 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -1,6 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; @@ -24,60 +30,108 @@ class DocumentListItem extends DocumentItem { @override Widget build(BuildContext context) { - return ListTile( - dense: true, - selected: isSelected, - onTap: () => _onTap(), - selectedTileColor: Theme.of(context).colorScheme.inversePrimary, - onLongPress: () => onSelected?.call(document), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Row( - children: [ - AbsorbPointer( - absorbing: isSelectionActive, - child: CorrespondentWidget( - isClickable: isLabelClickable, - correspondentId: document.correspondent, - onSelected: onCorrespondentSelected, + return DocumentTypeBlocProvider( + child: ListTile( + dense: true, + selected: isSelected, + onTap: () => _onTap(), + selectedTileColor: Theme.of(context).colorScheme.inversePrimary, + onLongPress: () => onSelected?.call(document), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + children: [ + AbsorbPointer( + absorbing: isSelectionActive, + child: CorrespondentWidget( + isClickable: isLabelClickable, + correspondentId: document.correspondent, + onSelected: onCorrespondentSelected, + ), ), + ], + ), + Text( + document.title, + overflow: TextOverflow.ellipsis, + maxLines: document.tags.isEmpty ? 2 : 1, + ), + AbsorbPointer( + absorbing: isSelectionActive, + child: TagsWidget( + isClickable: isLabelClickable, + tagIds: document.tags, + isMultiLine: false, + onTagSelected: (id) => onTagSelected?.call(id), ), - ], - ), - Text( - document.title, - overflow: TextOverflow.ellipsis, - maxLines: document.tags.isEmpty ? 2 : 1, - ), - ], - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: AbsorbPointer( - absorbing: isSelectionActive, - child: TagsWidget( - isClickable: isLabelClickable, - tagIds: document.tags, - isMultiLine: false, - onTagSelected: (id) => onTagSelected?.call(id), + ) + ], + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: + BlocBuilder, LabelState>( + builder: (context, docTypes) { + return RichText( + maxLines: 1, + overflow: TextOverflow.ellipsis, + text: TextSpan( + text: DateFormat.yMMMd().format(document.created), + style: Theme.of(context) + .textTheme + .labelSmall + ?.apply(color: Colors.grey), + children: document.documentType != null + ? [ + const TextSpan(text: '\u30FB'), + TextSpan( + text: + docTypes.labels[document.documentType]?.name, + ), + ] + : null, + ), + ); + }, + ) + // Row( + // children: [ + // Text( + // DateFormat.yMMMd().format(document.created), + // style: Theme.of(context) + // .textTheme + // .bodySmall + // ?.apply(color: Colors.grey), + // ), + // if (document.documentType != null) ...[ + // Text("\u30FB"), + // DocumentTypeWidget( + // documentTypeId: document.documentType, + // textStyle: Theme.of(context).textTheme.bodySmall?.apply( + // color: Colors.grey, + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ], + // ], + // ), + ), + isThreeLine: document.tags.isNotEmpty, + leading: AspectRatio( + aspectRatio: _a4AspectRatio, + child: GestureDetector( + child: DocumentPreview( + id: document.id, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + enableHero: enableHeroAnimation, + ), ), ), + contentPadding: const EdgeInsets.all(8.0), ), - isThreeLine: document.tags.isNotEmpty, - leading: AspectRatio( - aspectRatio: _a4AspectRatio, - child: GestureDetector( - child: DocumentPreview( - id: document.id, - fit: BoxFit.cover, - alignment: Alignment.topCenter, - enableHero: enableHeroAnimation, - ), - ), - ), - contentPadding: const EdgeInsets.all(8.0), ); } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index ae5311b..0e9729f 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -211,10 +211,15 @@ class _HomePageState extends State { MultiBlocProvider( providers: [ BlocProvider( - create: (context) => DocumentsCubit(context.read()), + create: (context) => DocumentsCubit( + context.read(), + context.read(), + ), ), BlocProvider( - create: (context) => SavedViewCubit(context.read()), + create: (context) => SavedViewCubit( + context.read(), + ), ), ], child: const DocumentsPage(), diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index f35d57f..8ad1c81 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; @@ -17,6 +18,8 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { _documentTypeRepository; final PaperlessDocumentsApi _documentsApi; + @override + final DocumentChangedNotifier notifier; final PaperlessServerStatsApi _statsApi; @@ -32,6 +35,7 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { this._correspondentRepository, this._documentTypeRepository, this._statsApi, + this.notifier, ) : super( InboxState( availableCorrespondents: @@ -41,6 +45,12 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { availableTags: _tagsRepository.current?.values ?? {}, ), ) { + _subscriptions.addAll( + notifier.listen( + onDeleted: remove, + onUpdated: replace, + ), + ); _subscriptions.add( _tagsRepository.values.listen((event) { if (event?.hasLoaded ?? false) { diff --git a/lib/features/linked_documents/bloc/linked_documents_cubit.dart b/lib/features/linked_documents/bloc/linked_documents_cubit.dart index ffc9343..bd66d05 100644 --- a/lib/features/linked_documents/bloc/linked_documents_cubit.dart +++ b/lib/features/linked_documents/bloc/linked_documents_cubit.dart @@ -1,5 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -8,8 +9,14 @@ class LinkedDocumentsCubit extends Cubit @override final PaperlessDocumentsApi api; - LinkedDocumentsCubit(this.api, DocumentFilter filter) - : super(const LinkedDocumentsState()) { + @override + final DocumentChangedNotifier notifier; + + LinkedDocumentsCubit( + this.api, + DocumentFilter filter, + this.notifier, + ) : super(const LinkedDocumentsState()) { updateFilter(filter: filter); } } diff --git a/lib/features/paged_document_view/paged_documents_mixin.dart b/lib/features/paged_document_view/paged_documents_mixin.dart index 687d410..22bebe1 100644 --- a/lib/features/paged_document_view/paged_documents_mixin.dart +++ b/lib/features/paged_document_view/paged_documents_mixin.dart @@ -1,15 +1,18 @@ import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'model/paged_documents_state.dart'; /// -/// Mixin which can be used on cubits which handle documents. This implements all paging and filtering logic. +/// Mixin which can be used on cubits that handle documents. +/// This implements all paging and filtering logic. /// mixin PagedDocumentsMixin on BlocBase { PaperlessDocumentsApi get api; + DocumentChangedNotifier get notifier; Future loadMore() async { if (state.isLastPageLoaded) { diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart index 0c4b7a4..2090c3d 100644 --- a/lib/features/saved_view/view/saved_view_list.dart +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; diff --git a/lib/main.dart b/lib/main.dart index 4f51e8e..4239ff2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart'; @@ -168,6 +169,7 @@ void main() async { Provider.value( value: localNotificationService, ), + Provider.value(value: DocumentChangedNotifier()), ], child: MultiRepositoryProvider( providers: [ diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index f612cdd..d872076 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -143,6 +143,11 @@ class DocumentFilter extends Equatable { return newFilter; } + /// + /// Checks whether the properties of [document] match the current filter criteria. + /// + bool includes(DocumentModel document) {} + int get appliedFiltersCount => [ documentType != initial.documentType, correspondent != initial.correspondent, From 4d7fab18392d2649ffe6259eeb8e3588cf322e65 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 01:04:13 +0100 Subject: [PATCH 18/25] Cleaned up code, implemented message queue to notify subscribers of document updates. --- android/app/build.gradle | 10 +- ios/Podfile.lock | 44 ++- ios/Runner.xcodeproj/project.pbxproj | 5 +- ios/Runner/Info.plist | 4 +- .../notifier/document_changed_notifier.dart | 34 +- lib/core/repository/base_repository.dart | 22 +- .../impl/correspondent_repository_impl.dart | 17 +- .../impl/document_type_repository_impl.dart | 17 +- .../impl/saved_view_repository_impl.dart | 10 +- .../impl/storage_path_repository_impl.dart | 16 +- .../repository/impl/tag_repository_impl.dart | 18 +- lib/core/repository/label_repository.dart | 7 +- .../provider/label_repositories_provider.dart | 12 +- .../repository/saved_view_repository.dart | 4 +- .../impl/correspondent_repository_state.dart | 4 +- .../correspondent_repository_state.g.dart | 2 +- .../impl/document_type_repository_state.dart | 11 +- .../document_type_repository_state.g.dart | 2 +- .../impl/saved_view_repository_state.dart | 4 +- .../impl/saved_view_repository_state.g.dart | 2 +- .../impl/storage_path_repository_state.dart | 11 +- .../impl/storage_path_repository_state.g.dart | 2 +- .../state/impl/tag_repository_state.dart | 9 +- .../state/impl/tag_repository_state.g.dart | 2 +- .../state/indexed_repository_state.dart | 16 + .../repository/state/repository_state.dart | 16 - lib/core/service/github_issue_service.dart | 2 +- .../widgets/material/search/m3_search.dart | 6 +- lib/core/widgets/shimmer_placeholder.dart | 21 ++ .../bloc/document_details_cubit.dart | 26 +- .../view/pages/document_details_page.dart | 33 +- .../cubit/document_search_cubit.dart | 14 +- .../view/document_search_page.dart | 10 +- .../cubit/document_upload_cubit.dart | 16 +- .../document_upload_preparation_page.dart | 14 +- .../documents/bloc/documents_cubit.dart | 29 +- .../documents/bloc/documents_state.dart | 7 +- .../view/pages/document_edit_page.dart | 9 +- .../documents/view/pages/documents_page.dart | 33 +- .../view/widgets/adaptive_documents_view.dart | 27 +- .../widgets/document_grid_loading_widget.dart | 102 ++++++ .../documents_list_loading_widget.dart | 93 +++-- .../widgets/items/document_list_item.dart | 2 +- .../document_item_placeholder.dart | 30 ++ .../widgets/placeholder/tags_placeholder.dart | 37 ++ .../widgets/placeholder/text_placeholder.dart | 26 ++ .../view/widgets/sort_documents_button.dart | 8 +- .../cubit/edit_document_cubit.dart | 37 +- .../edit_label/cubit/edit_label_cubit.dart | 6 +- .../edit_label/view/add_label_page.dart | 5 +- .../edit_label/view/edit_label_page.dart | 5 +- .../view/impl/add_correspondent_page.dart | 3 +- .../view/impl/add_document_type_page.dart | 3 +- .../view/impl/add_storage_path_page.dart | 3 +- .../edit_label/view/impl/add_tag_page.dart | 2 +- .../view/impl/edit_correspondent_page.dart | 3 +- .../view/impl/edit_document_type_page.dart | 3 +- .../view/impl/edit_storage_path_page.dart | 3 +- .../edit_label/view/impl/edit_tag_page.dart | 2 +- lib/features/home/view/home_page.dart | 34 +- .../view/widget/verify_identity_page.dart | 14 +- lib/features/inbox/bloc/inbox_cubit.dart | 87 ++--- .../inbox/bloc/state/inbox_state.dart | 4 +- lib/features/inbox/view/pages/inbox_page.dart | 199 +++++------ .../view/widgets/inbox_empty_widget.dart | 2 +- .../inbox/view/widgets/inbox_item.dart | 33 +- lib/features/labels/bloc/label_cubit.dart | 8 +- .../correspondent_bloc_provider.dart | 8 +- .../document_type_bloc_provider.dart | 3 +- .../bloc/providers/labels_bloc_provider.dart | 11 +- .../providers/storage_path_bloc_provider.dart | 3 +- .../bloc/providers/tag_bloc_provider.dart | 2 +- .../tags/view/widgets/tags_form_field.dart | 3 +- .../labels/view/pages/labels_page.dart | 320 ++++++++---------- .../labels/view/widgets/label_item.dart | 3 +- .../labels/view/widgets/label_tab_view.dart | 107 +++--- .../labels/view/widgets/label_text.dart | 7 +- .../bloc/linked_documents_cubit.dart | 19 +- .../view/pages/linked_documents_page.dart | 13 +- .../services/local_notification_service.dart | 3 +- .../paged_documents_mixin.dart | 66 ++-- .../saved_view/cubit/saved_view_cubit.dart | 4 +- .../cubit/saved_view_details_cubit.dart | 12 +- .../saved_view/view/saved_view_list.dart | 1 + .../saved_view/view/saved_view_page.dart | 10 +- lib/features/scan/view/scanner_page.dart | 70 ++-- .../view/dialogs/account_settings_dialog.dart | 23 +- .../cubit/similar_documents_cubit.dart | 17 +- .../view}/similar_documents_view.dart | 36 +- lib/l10n/intl_cs.arb | 6 + lib/l10n/intl_de.arb | 6 + lib/l10n/intl_en.arb | 6 + lib/l10n/intl_pl.arb | 6 + lib/l10n/intl_tr.arb | 6 + lib/main.dart | 14 +- lib/routes/document_details_route.dart | 5 +- .../lib/src/models/document_filter.dart | 15 +- .../lib/src/models/paged_search_result.dart | 8 +- .../absolute_date_range_query.dart | 13 + .../date_range_queries/date_range_query.dart | 2 + .../relative_date_range_query.dart | 20 ++ .../unset_date_range_query.dart | 3 + .../query_parameters/id_query_parameter.dart | 12 +- .../tags_query/any_assigned_tags_query.dart | 5 + .../tags_query/ids_tags_query.dart | 8 +- .../only_not_assigned_tags_query.dart | 5 + .../tags_query/tags_query.dart | 2 + .../models/query_parameters/text_query.dart | 20 ++ packages/paperless_api/pubspec.yaml | 1 + pubspec.lock | 234 +++++++------ pubspec.yaml | 1 - 111 files changed, 1412 insertions(+), 1029 deletions(-) create mode 100644 lib/core/repository/state/indexed_repository_state.dart delete mode 100644 lib/core/repository/state/repository_state.dart create mode 100644 lib/core/widgets/shimmer_placeholder.dart create mode 100644 lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart create mode 100644 lib/features/documents/view/widgets/placeholder/tags_placeholder.dart create mode 100644 lib/features/documents/view/widgets/placeholder/text_placeholder.dart rename lib/features/{document_details/view/pages => similar_documents/view}/similar_documents_view.dart (77%) diff --git a/android/app/build.gradle b/android/app/build.gradle index c7a26c2..43710e2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -68,11 +68,11 @@ android { storePassword keystoreProperties['storePassword'] } } - buildTypes { - release { - signingConfig signingConfigs.release - } - } + buildTypes { + release { + signingConfig signingConfigs.debug + } + } } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 842ea55..2c66854 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -35,7 +35,7 @@ PODS: - DKPhotoGallery/Resource (0.0.17): - SDWebImage - SwiftyGif - - edge_detection (1.0.9): + - edge_detection (1.1.1): - Flutter - WeScan - file_picker (0.0.1): @@ -44,6 +44,8 @@ PODS: - Flutter (1.0.0) - flutter_keyboard_visibility (0.0.1): - Flutter + - flutter_local_notifications (0.0.1): + - Flutter - flutter_native_splash (0.0.1): - Flutter - fluttertoast (0.0.2): @@ -56,10 +58,13 @@ PODS: - Flutter - local_auth_ios (0.0.1): - Flutter + - open_filex (0.0.2): + - Flutter - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter + - FlutterMacOS - pdfx (1.0.0): - Flutter - permission_handler_apple (9.0.4): @@ -72,8 +77,9 @@ PODS: - SDWebImage/Core (5.13.5) - share_plus (0.0.1): - Flutter - - shared_preferences_ios (0.0.1): + - shared_preferences_foundation (0.0.1): - Flutter + - FlutterMacOS - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) @@ -90,17 +96,19 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) + - open_filex (from `.symlinks/plugins/open_filex/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - pdfx (from `.symlinks/plugins/pdfx/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -128,6 +136,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_keyboard_visibility: :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" fluttertoast: @@ -136,10 +146,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" + open_filex: + :path: ".symlinks/plugins/open_filex/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" pdfx: :path: ".symlinks/plugins/pdfx/ios" permission_handler_apple: @@ -148,8 +160,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/receive_sharing_intent/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" - shared_preferences_ios: - :path: ".symlinks/plugins/shared_preferences_ios/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: @@ -160,28 +172,30 @@ SPEC CHECKSUMS: device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - edge_detection: 9bc5ee35073b5a17c0b3b679908f01017ce3062a - file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 + edge_detection: fa02aa120e00d87ada0ca2430b6c6087a501b1e9 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b + fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 - shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2 WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 664648c..1653b15 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -321,10 +321,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -335,6 +337,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6cb665f..a837d1a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -65,5 +65,7 @@ CADisableMinimumFrameDurationOnPhone - + UIApplicationSupportsIndirectInputEvents + + diff --git a/lib/core/notifier/document_changed_notifier.dart b/lib/core/notifier/document_changed_notifier.dart index 6597d6f..c53dedc 100644 --- a/lib/core/notifier/document_changed_notifier.dart +++ b/lib/core/notifier/document_changed_notifier.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:developer'; +import 'package:flutter/foundation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:rxdart/subjects.dart'; @@ -9,26 +11,40 @@ class DocumentChangedNotifier { final Subject _updated = PublishSubject(); final Subject _deleted = PublishSubject(); + final Map> _subscribers = {}; + void notifyUpdated(DocumentModel updated) { + debugPrint("Notifying updated document ${updated.id}"); _updated.add(updated); } void notifyDeleted(DocumentModel deleted) { + debugPrint("Notifying deleted document ${deleted.id}"); _deleted.add(deleted); } - List listen({ + void subscribe( + dynamic subscriber, { DocumentChangedCallback? onUpdated, DocumentChangedCallback? onDeleted, }) { - return [ - _updated.listen((value) { - onUpdated?.call(value); - }), - _updated.listen((value) { - onDeleted?.call(value); - }), - ]; + _subscribers.putIfAbsent( + subscriber, + () => [ + _updated.listen((value) { + onUpdated?.call(value); + }), + _deleted.listen((value) { + onDeleted?.call(value); + }), + ], + ); + } + + void unsubscribe(dynamic subscriber) { + _subscribers[subscriber]?.forEach((element) { + element.cancel(); + }); } void close() { diff --git a/lib/core/repository/base_repository.dart b/lib/core/repository/base_repository.dart index f0c4589..1eb57c4 100644 --- a/lib/core/repository/base_repository.dart +++ b/lib/core/repository/base_repository.dart @@ -1,30 +1,30 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:rxdart/subjects.dart'; /// /// Base repository class which all repositories should implement /// -abstract class BaseRepository - extends Cubit with HydratedMixin { - final State _initialState; +abstract class BaseRepository extends Cubit> + with HydratedMixin { + final IndexedRepositoryState _initialState; BaseRepository(this._initialState) : super(_initialState) { hydrate(); } - Stream get values => + Stream?> get values => BehaviorSubject.seeded(state)..addStream(super.stream); - State? get current => state; + IndexedRepositoryState? get current => state; bool get isInitialized => state.hasLoaded; - Future create(Type object); - Future find(int id); - Future> findAll([Iterable? ids]); - Future update(Type object); - Future delete(Type object); + Future create(T object); + Future find(int id); + Future> findAll([Iterable? ids]); + Future update(T object); + Future delete(T object); @override Future clear() async { diff --git a/lib/core/repository/impl/correspondent_repository_impl.dart b/lib/core/repository/impl/correspondent_repository_impl.dart index 4b676ac..7227c58 100644 --- a/lib/core/repository/impl/correspondent_repository_impl.dart +++ b/lib/core/repository/impl/correspondent_repository_impl.dart @@ -3,10 +3,8 @@ import 'dart:async'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; -class CorrespondentRepositoryImpl - extends LabelRepository { +class CorrespondentRepositoryImpl extends LabelRepository { final PaperlessLabelsApi _api; CorrespondentRepositoryImpl(this._api) @@ -15,7 +13,7 @@ class CorrespondentRepositoryImpl @override Future create(Correspondent correspondent) async { final created = await _api.saveCorrespondent(correspondent); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -24,7 +22,7 @@ class CorrespondentRepositoryImpl @override Future delete(Correspondent correspondent) async { await _api.deleteCorrespondent(correspondent); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..removeWhere((k, v) => k == correspondent.id); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return correspondent.id!; @@ -34,7 +32,7 @@ class CorrespondentRepositoryImpl Future find(int id) async { final correspondent = await _api.getCorrespondent(id); if (correspondent != null) { - final updatedState = {...state.values}..[id] = correspondent; + final updatedState = {...state.values ?? {}}..[id] = correspondent; emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return correspondent; } @@ -44,7 +42,7 @@ class CorrespondentRepositoryImpl @override Future> findAll([Iterable? ids]) async { final correspondents = await _api.getCorrespondents(ids); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..addEntries(correspondents.map((e) => MapEntry(e.id!, e))); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return correspondents; @@ -53,7 +51,8 @@ class CorrespondentRepositoryImpl @override Future update(Correspondent correspondent) async { final updated = await _api.updateCorrespondent(correspondent); - final updatedState = {...state.values}..update(updated.id!, (_) => updated); + final updatedState = {...state.values ?? {}} + ..update(updated.id!, (_) => updated); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); return updated; } @@ -64,7 +63,7 @@ class CorrespondentRepositoryImpl } @override - Map toJson(CorrespondentRepositoryState state) { + Map toJson(covariant CorrespondentRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/impl/document_type_repository_impl.dart b/lib/core/repository/impl/document_type_repository_impl.dart index 1e1ae92..5fd7a87 100644 --- a/lib/core/repository/impl/document_type_repository_impl.dart +++ b/lib/core/repository/impl/document_type_repository_impl.dart @@ -1,10 +1,8 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:rxdart/rxdart.dart' show BehaviorSubject; -class DocumentTypeRepositoryImpl - extends LabelRepository { +class DocumentTypeRepositoryImpl extends LabelRepository { final PaperlessLabelsApi _api; DocumentTypeRepositoryImpl(this._api) @@ -13,7 +11,7 @@ class DocumentTypeRepositoryImpl @override Future create(DocumentType documentType) async { final created = await _api.saveDocumentType(documentType); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -22,7 +20,7 @@ class DocumentTypeRepositoryImpl @override Future delete(DocumentType documentType) async { await _api.deleteDocumentType(documentType); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..removeWhere((k, v) => k == documentType.id); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return documentType.id!; @@ -32,7 +30,7 @@ class DocumentTypeRepositoryImpl Future find(int id) async { final documentType = await _api.getDocumentType(id); if (documentType != null) { - final updatedState = {...state.values}..[id] = documentType; + final updatedState = {...state.values ?? {}}..[id] = documentType; emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return documentType; } @@ -42,7 +40,7 @@ class DocumentTypeRepositoryImpl @override Future> findAll([Iterable? ids]) async { final documentTypes = await _api.getDocumentTypes(ids); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return documentTypes; @@ -51,7 +49,8 @@ class DocumentTypeRepositoryImpl @override Future update(DocumentType documentType) async { final updated = await _api.updateDocumentType(documentType); - final updatedState = {...state.values}..update(updated.id!, (_) => updated); + final updatedState = {...state.values ?? {}} + ..update(updated.id!, (_) => updated); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); return updated; } @@ -62,7 +61,7 @@ class DocumentTypeRepositoryImpl } @override - Map toJson(DocumentTypeRepositoryState state) { + Map toJson(covariant DocumentTypeRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/impl/saved_view_repository_impl.dart b/lib/core/repository/impl/saved_view_repository_impl.dart index b5e03aa..18eceed 100644 --- a/lib/core/repository/impl/saved_view_repository_impl.dart +++ b/lib/core/repository/impl/saved_view_repository_impl.dart @@ -10,7 +10,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { @override Future create(SavedView object) async { final created = await _api.save(object); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -19,7 +19,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { @override Future delete(SavedView view) async { await _api.delete(view); - final updatedState = {...state.values}..remove(view.id); + final updatedState = {...state.values ?? {}}..remove(view.id); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); return view.id!; } @@ -27,7 +27,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { @override Future find(int id) async { final found = await _api.find(id); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..update(id, (_) => found, ifAbsent: () => found); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); return found; @@ -37,7 +37,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { Future> findAll([Iterable? ids]) async { final found = await _api.findAll(ids); final updatedState = { - ...state.values, + ...state.values ?? {}, ...{for (final view in found) view.id!: view}, }; emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); @@ -56,7 +56,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository { } @override - Map toJson(SavedViewRepositoryState state) { + Map toJson(covariant SavedViewRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/impl/storage_path_repository_impl.dart b/lib/core/repository/impl/storage_path_repository_impl.dart index b738827..1fb54ea 100644 --- a/lib/core/repository/impl/storage_path_repository_impl.dart +++ b/lib/core/repository/impl/storage_path_repository_impl.dart @@ -3,8 +3,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:rxdart/rxdart.dart' show BehaviorSubject; -class StoragePathRepositoryImpl - extends LabelRepository { +class StoragePathRepositoryImpl extends LabelRepository { final PaperlessLabelsApi _api; StoragePathRepositoryImpl(this._api) @@ -13,7 +12,7 @@ class StoragePathRepositoryImpl @override Future create(StoragePath storagePath) async { final created = await _api.saveStoragePath(storagePath); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -22,7 +21,7 @@ class StoragePathRepositoryImpl @override Future delete(StoragePath storagePath) async { await _api.deleteStoragePath(storagePath); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..removeWhere((k, v) => k == storagePath.id); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return storagePath.id!; @@ -32,7 +31,7 @@ class StoragePathRepositoryImpl Future find(int id) async { final storagePath = await _api.getStoragePath(id); if (storagePath != null) { - final updatedState = {...state.values}..[id] = storagePath; + final updatedState = {...state.values ?? {}}..[id] = storagePath; emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return storagePath; } @@ -42,7 +41,7 @@ class StoragePathRepositoryImpl @override Future> findAll([Iterable? ids]) async { final storagePaths = await _api.getStoragePaths(ids); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return storagePaths; @@ -51,7 +50,8 @@ class StoragePathRepositoryImpl @override Future update(StoragePath storagePath) async { final updated = await _api.updateStoragePath(storagePath); - final updatedState = {...state.values}..update(updated.id!, (_) => updated); + final updatedState = {...state.values ?? {}} + ..update(updated.id!, (_) => updated); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); return updated; } @@ -62,7 +62,7 @@ class StoragePathRepositoryImpl } @override - Map toJson(StoragePathRepositoryState state) { + Map toJson(covariant StoragePathRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/impl/tag_repository_impl.dart b/lib/core/repository/impl/tag_repository_impl.dart index 09f6061..a39a77b 100644 --- a/lib/core/repository/impl/tag_repository_impl.dart +++ b/lib/core/repository/impl/tag_repository_impl.dart @@ -1,10 +1,8 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; -class TagRepositoryImpl extends LabelRepository { +class TagRepositoryImpl extends LabelRepository { final PaperlessLabelsApi _api; TagRepositoryImpl(this._api) : super(const TagRepositoryState()); @@ -12,7 +10,7 @@ class TagRepositoryImpl extends LabelRepository { @override Future create(Tag object) async { final created = await _api.saveTag(object); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..putIfAbsent(created.id!, () => created); emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return created; @@ -21,7 +19,8 @@ class TagRepositoryImpl extends LabelRepository { @override Future delete(Tag tag) async { await _api.deleteTag(tag); - final updatedState = {...state.values}..removeWhere((k, v) => k == tag.id); + final updatedState = {...state.values ?? {}} + ..removeWhere((k, v) => k == tag.id); emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return tag.id!; } @@ -30,7 +29,7 @@ class TagRepositoryImpl extends LabelRepository { Future find(int id) async { final tag = await _api.getTag(id); if (tag != null) { - final updatedState = {...state.values}..[id] = tag; + final updatedState = {...state.values ?? {}}..[id] = tag; emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return tag; } @@ -40,7 +39,7 @@ class TagRepositoryImpl extends LabelRepository { @override Future> findAll([Iterable? ids]) async { final tags = await _api.getTags(ids); - final updatedState = {...state.values} + final updatedState = {...state.values ?? {}} ..addEntries(tags.map((e) => MapEntry(e.id!, e))); emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return tags; @@ -49,7 +48,8 @@ class TagRepositoryImpl extends LabelRepository { @override Future update(Tag tag) async { final updated = await _api.updateTag(tag); - final updatedState = {...state.values}..update(updated.id!, (_) => updated); + final updatedState = {...state.values ?? {}} + ..update(updated.id!, (_) => updated); emit(TagRepositoryState(values: updatedState, hasLoaded: true)); return updated; } @@ -60,7 +60,7 @@ class TagRepositoryImpl extends LabelRepository { } @override - Map? toJson(TagRepositoryState state) { + Map? toJson(covariant TagRepositoryState state) { return state.toJson(); } } diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index c2aa3bc..8fe3458 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -1,8 +1,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/base_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; -abstract class LabelRepository - extends BaseRepository { - LabelRepository(State initial) : super(initial); +abstract class LabelRepository extends BaseRepository { + LabelRepository(IndexedRepositoryState initial) : super(initial); } diff --git a/lib/core/repository/provider/label_repositories_provider.dart b/lib/core/repository/provider/label_repositories_provider.dart index d45c792..e9634be 100644 --- a/lib/core/repository/provider/label_repositories_provider.dart +++ b/lib/core/repository/provider/label_repositories_provider.dart @@ -17,20 +17,16 @@ class LabelRepositoriesProvider extends StatelessWidget { return MultiRepositoryProvider( providers: [ RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), ), RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), ), RepositoryProvider( - create: (context) => context - .read>(), + create: (context) => context.read>(), ), RepositoryProvider( - create: (context) => - context.read>(), + create: (context) => context.read>(), ), ], child: child, diff --git a/lib/core/repository/saved_view_repository.dart b/lib/core/repository/saved_view_repository.dart index 644f367..bb1c4e3 100644 --- a/lib/core/repository/saved_view_repository.dart +++ b/lib/core/repository/saved_view_repository.dart @@ -1,8 +1,8 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/base_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; -abstract class SavedViewRepository - extends BaseRepository { +abstract class SavedViewRepository extends BaseRepository { SavedViewRepository(super.initialState); } diff --git a/lib/core/repository/state/impl/correspondent_repository_state.dart b/lib/core/repository/state/impl/correspondent_repository_state.dart index 5fb88ee..fce9efb 100644 --- a/lib/core/repository/state/impl/correspondent_repository_state.dart +++ b/lib/core/repository/state/impl/correspondent_repository_state.dart @@ -1,13 +1,13 @@ import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; part 'correspondent_repository_state.g.dart'; @JsonSerializable() class CorrespondentRepositoryState - extends RepositoryState> { + extends IndexedRepositoryState { const CorrespondentRepositoryState({ super.values = const {}, super.hasLoaded, diff --git a/lib/core/repository/state/impl/correspondent_repository_state.g.dart b/lib/core/repository/state/impl/correspondent_repository_state.g.dart index 08e2976..405f4ff 100644 --- a/lib/core/repository/state/impl/correspondent_repository_state.g.dart +++ b/lib/core/repository/state/impl/correspondent_repository_state.g.dart @@ -20,6 +20,6 @@ CorrespondentRepositoryState _$CorrespondentRepositoryStateFromJson( Map _$CorrespondentRepositoryStateToJson( CorrespondentRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/impl/document_type_repository_state.dart b/lib/core/repository/state/impl/document_type_repository_state.dart index 7ce5188..4a4ab1f 100644 --- a/lib/core/repository/state/impl/document_type_repository_state.dart +++ b/lib/core/repository/state/impl/document_type_repository_state.dart @@ -1,20 +1,21 @@ import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:json_annotation/json_annotation.dart'; part 'document_type_repository_state.g.dart'; @JsonSerializable() -class DocumentTypeRepositoryState - extends RepositoryState> { +class DocumentTypeRepositoryState extends IndexedRepositoryState { const DocumentTypeRepositoryState({ super.values = const {}, super.hasLoaded, }); @override - DocumentTypeRepositoryState copyWith( - {Map? values, bool? hasLoaded}) { + DocumentTypeRepositoryState copyWith({ + Map? values, + bool? hasLoaded, + }) { return DocumentTypeRepositoryState( values: values ?? this.values, hasLoaded: hasLoaded ?? this.hasLoaded, diff --git a/lib/core/repository/state/impl/document_type_repository_state.g.dart b/lib/core/repository/state/impl/document_type_repository_state.g.dart index 6868bd6..3528b96 100644 --- a/lib/core/repository/state/impl/document_type_repository_state.g.dart +++ b/lib/core/repository/state/impl/document_type_repository_state.g.dart @@ -20,6 +20,6 @@ DocumentTypeRepositoryState _$DocumentTypeRepositoryStateFromJson( Map _$DocumentTypeRepositoryStateToJson( DocumentTypeRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/impl/saved_view_repository_state.dart b/lib/core/repository/state/impl/saved_view_repository_state.dart index ecd9e49..9cd7672 100644 --- a/lib/core/repository/state/impl/saved_view_repository_state.dart +++ b/lib/core/repository/state/impl/saved_view_repository_state.dart @@ -1,11 +1,11 @@ import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:json_annotation/json_annotation.dart'; part 'saved_view_repository_state.g.dart'; @JsonSerializable() -class SavedViewRepositoryState extends RepositoryState> { +class SavedViewRepositoryState extends IndexedRepositoryState { const SavedViewRepositoryState({ super.values = const {}, super.hasLoaded = false, diff --git a/lib/core/repository/state/impl/saved_view_repository_state.g.dart b/lib/core/repository/state/impl/saved_view_repository_state.g.dart index 4cc61b9..bfcc949 100644 --- a/lib/core/repository/state/impl/saved_view_repository_state.g.dart +++ b/lib/core/repository/state/impl/saved_view_repository_state.g.dart @@ -20,6 +20,6 @@ SavedViewRepositoryState _$SavedViewRepositoryStateFromJson( Map _$SavedViewRepositoryStateToJson( SavedViewRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/impl/storage_path_repository_state.dart b/lib/core/repository/state/impl/storage_path_repository_state.dart index 366db8e..b9ed856 100644 --- a/lib/core/repository/state/impl/storage_path_repository_state.dart +++ b/lib/core/repository/state/impl/storage_path_repository_state.dart @@ -1,20 +1,21 @@ import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:json_annotation/json_annotation.dart'; part 'storage_path_repository_state.g.dart'; @JsonSerializable() -class StoragePathRepositoryState - extends RepositoryState> { +class StoragePathRepositoryState extends IndexedRepositoryState { const StoragePathRepositoryState({ super.values = const {}, super.hasLoaded = false, }); @override - StoragePathRepositoryState copyWith( - {Map? values, bool? hasLoaded}) { + StoragePathRepositoryState copyWith({ + Map? values, + bool? hasLoaded, + }) { return StoragePathRepositoryState( values: values ?? this.values, hasLoaded: hasLoaded ?? this.hasLoaded, diff --git a/lib/core/repository/state/impl/storage_path_repository_state.g.dart b/lib/core/repository/state/impl/storage_path_repository_state.g.dart index 4be8ad5..75ac365 100644 --- a/lib/core/repository/state/impl/storage_path_repository_state.g.dart +++ b/lib/core/repository/state/impl/storage_path_repository_state.g.dart @@ -20,6 +20,6 @@ StoragePathRepositoryState _$StoragePathRepositoryStateFromJson( Map _$StoragePathRepositoryStateToJson( StoragePathRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/impl/tag_repository_state.dart b/lib/core/repository/state/impl/tag_repository_state.dart index 6e0e261..4558bfe 100644 --- a/lib/core/repository/state/impl/tag_repository_state.dart +++ b/lib/core/repository/state/impl/tag_repository_state.dart @@ -1,18 +1,21 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; part 'tag_repository_state.g.dart'; @JsonSerializable() -class TagRepositoryState extends RepositoryState> { +class TagRepositoryState extends IndexedRepositoryState { const TagRepositoryState({ super.values = const {}, super.hasLoaded = false, }); @override - TagRepositoryState copyWith({Map? values, bool? hasLoaded}) { + TagRepositoryState copyWith({ + Map? values, + bool? hasLoaded, + }) { return TagRepositoryState( values: values ?? this.values, hasLoaded: hasLoaded ?? this.hasLoaded, diff --git a/lib/core/repository/state/impl/tag_repository_state.g.dart b/lib/core/repository/state/impl/tag_repository_state.g.dart index 02e8bd0..09e04ee 100644 --- a/lib/core/repository/state/impl/tag_repository_state.g.dart +++ b/lib/core/repository/state/impl/tag_repository_state.g.dart @@ -18,6 +18,6 @@ TagRepositoryState _$TagRepositoryStateFromJson(Map json) => Map _$TagRepositoryStateToJson(TagRepositoryState instance) => { - 'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), + 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)), 'hasLoaded': instance.hasLoaded, }; diff --git a/lib/core/repository/state/indexed_repository_state.dart b/lib/core/repository/state/indexed_repository_state.dart new file mode 100644 index 0000000..d3caee5 --- /dev/null +++ b/lib/core/repository/state/indexed_repository_state.dart @@ -0,0 +1,16 @@ +abstract class IndexedRepositoryState { + final Map? values; + final bool hasLoaded; + + const IndexedRepositoryState({ + required this.values, + this.hasLoaded = false, + }) : assert(!(values == null) || !hasLoaded); + + IndexedRepositoryState.loaded(this.values) : hasLoaded = true; + + IndexedRepositoryState copyWith({ + Map? values, + bool? hasLoaded, + }); +} diff --git a/lib/core/repository/state/repository_state.dart b/lib/core/repository/state/repository_state.dart deleted file mode 100644 index 7498a33..0000000 --- a/lib/core/repository/state/repository_state.dart +++ /dev/null @@ -1,16 +0,0 @@ -abstract class RepositoryState { - final T values; - final bool hasLoaded; - - const RepositoryState({ - required this.values, - this.hasLoaded = false, - }); - - RepositoryState.loaded(this.values) : hasLoaded = true; - - RepositoryState copyWith({ - T? values, - bool? hasLoaded, - }); -} diff --git a/lib/core/service/github_issue_service.dart b/lib/core/service/github_issue_service.dart index 38568b9..b4e1d79 100644 --- a/lib/core/service/github_issue_service.dart +++ b/lib/core/service/github_issue_service.dart @@ -27,7 +27,7 @@ class GithubIssueService { ..tryPutIfAbsent('assignees', () => assignees?.join(',')) ..tryPutIfAbsent('project', () => project), ); - log("[GitHubIssueService] Creating GitHub issue: " + uri.toString()); + debugPrint("[GitHubIssueService] Creating GitHub issue: " + uri.toString()); launchUrl( uri, mode: LaunchMode.externalApplication, diff --git a/lib/core/widgets/material/search/m3_search.dart b/lib/core/widgets/material/search/m3_search.dart index 43572dc..dc01985 100644 --- a/lib/core/widgets/material/search/m3_search.dart +++ b/lib/core/widgets/material/search/m3_search.dart @@ -4,6 +4,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// Shows a full screen search page and returns the search result selected by /// the user when the page is closed. @@ -221,12 +222,13 @@ abstract class SearchDelegate { final ColorScheme colorScheme = theme.colorScheme; return theme.copyWith( appBarTheme: AppBarTheme( - brightness: colorScheme.brightness, + systemOverlayStyle: colorScheme.brightness == Brightness.light + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, backgroundColor: colorScheme.brightness == Brightness.dark ? Colors.grey[900] : Colors.white, iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), - textTheme: theme.textTheme, ), inputDecorationTheme: searchFieldDecorationTheme ?? InputDecorationTheme( diff --git a/lib/core/widgets/shimmer_placeholder.dart b/lib/core/widgets/shimmer_placeholder.dart new file mode 100644 index 0000000..d3b41be --- /dev/null +++ b/lib/core/widgets/shimmer_placeholder.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class ShimmerPlaceholder extends StatelessWidget { + final Widget child; + + const ShimmerPlaceholder({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Theme.of(context).brightness == Brightness.light + ? Colors.grey[300]! + : Colors.grey[900]!, + highlightColor: Theme.of(context).brightness == Brightness.light + ? Colors.grey[100]! + : Colors.grey[600]!, + child: child, + ); + } +} diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart index 68772c8..eaed8d2 100644 --- a/lib/features/document_details/bloc/document_details_cubit.dart +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -1,23 +1,32 @@ +import 'dart:async'; import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; part 'document_details_state.dart'; class DocumentDetailsCubit extends Cubit { final PaperlessDocumentsApi _api; + final DocumentChangedNotifier _notifier; - DocumentDetailsCubit(this._api, DocumentModel initialDocument) - : super(DocumentDetailsState(document: initialDocument)) { + final List _subscriptions = []; + DocumentDetailsCubit( + this._api, + this._notifier, { + required DocumentModel initialDocument, + }) : super(DocumentDetailsState(document: initialDocument)) { + _notifier.subscribe(this, onUpdated: replace); loadSuggestions(); } Future delete(DocumentModel document) async { await _api.delete(document); + _notifier.notifyDeleted(document); } Future loadSuggestions() async { @@ -41,7 +50,7 @@ class DocumentDetailsCubit extends Cubit { final int asn = await _api.findNextAsn(); final updatedDocument = await _api.update(document.copyWith(archiveSerialNumber: asn)); - emit(state.copyWith(document: updatedDocument)); + _notifier.notifyUpdated(updatedDocument); } } @@ -60,7 +69,16 @@ class DocumentDetailsCubit extends Cubit { ); } - void replaceDocument(DocumentModel document) { + void replace(DocumentModel document) { emit(state.copyWith(document: document)); } + + @override + Future close() { + for (final element in _subscriptions) { + element.cancel(); + } + _notifier.unsubscribe(this); + return super.close(); + } } 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 203ac30..648d70a 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'dart:math'; +import 'package:badges/badges.dart' as b; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,13 +8,11 @@ import 'package:intl/intl.dart'; import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; -import 'package:paperless_mobile/features/document_details/view/pages/similar_documents_view.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; @@ -30,9 +28,7 @@ import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:badges/badges.dart' as b; - -import '../../../../core/repository/state/impl/document_type_repository_state.dart'; +import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart'; //TODO: Refactor this into several widgets class DocumentDetailsPage extends StatefulWidget { @@ -79,16 +75,7 @@ class _DocumentDetailsPageState extends State { body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverAppBar( - leading: IconButton( - icon: const Icon( - Icons.arrow_back, - color: Colors - .black, //TODO: check if there is a way to dynamically determine color... - ), - onPressed: () => Navigator.of(context).pop( - context.read().state.document, - ), - ), + leading: const BackButton(), floating: true, pinned: true, expandedHeight: 200.0, @@ -153,6 +140,7 @@ class _DocumentDetailsPageState extends State { builder: (context, state) { return BlocProvider( create: (context) => SimilarDocumentsCubit( + context.read(), context.read(), documentId: state.document.id, ), @@ -168,7 +156,7 @@ class _DocumentDetailsPageState extends State { _buildDocumentMetaDataView( state.document, ), - _buildSimilarDocumentsView(), + const SimilarDocumentsView(), ], ), ).paddedSymmetrically(horizontal: 8); @@ -284,6 +272,7 @@ class _DocumentDetailsPageState extends State { documentTypeRepository: context.read(), storagePathRepository: context.read(), tagRepository: context.read(), + notifier: context.read(), ), ), BlocProvider.value( @@ -294,7 +283,7 @@ class _DocumentDetailsPageState extends State { listenWhen: (previous, current) => previous.document != current.document, listener: (context, state) { - cubit.replaceDocument(state.document); + cubit.replace(state.document); }, child: BlocBuilder( builder: (context, state) { @@ -461,7 +450,7 @@ class _DocumentDetailsPageState extends State { visible: document.documentType != null, child: _DetailsItem( label: S.of(context).documentDocumentTypePropertyLabel, - content: LabelText( + content: LabelText( style: Theme.of(context).textTheme.bodyLarge, id: document.documentType, ), @@ -471,7 +460,7 @@ class _DocumentDetailsPageState extends State { visible: document.correspondent != null, child: _DetailsItem( label: S.of(context).documentCorrespondentPropertyLabel, - content: LabelText( + content: LabelText( style: Theme.of(context).textTheme.bodyLarge, id: document.correspondent, ), @@ -555,10 +544,6 @@ class _DocumentDetailsPageState extends State { ), ); } - - Widget _buildSimilarDocumentsView() { - return const SimilarDocumentsView(); - } } class _DetailsItem extends StatelessWidget { diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart index 79300f2..616d46f 100644 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -13,7 +13,13 @@ class DocumentSearchCubit extends HydratedCubit final DocumentChangedNotifier notifier; DocumentSearchCubit(this.api, this.notifier) - : super(const DocumentSearchState()); + : super(const DocumentSearchState()) { + notifier.subscribe( + this, + onDeleted: remove, + onUpdated: replace, + ); + } Future search(String query) async { emit(state.copyWith( @@ -61,6 +67,12 @@ class DocumentSearchCubit extends HydratedCubit )); } + @override + Future close() { + notifier.unsubscribe(this); + return super.close(); + } + @override DocumentSearchState? fromJson(Map json) { return DocumentSearchState.fromJson(json); diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 2f0cf21..ed2ee3b 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -158,18 +158,16 @@ class _DocumentSearchPageState extends State { isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, - onTap: (document) async { - final updatedDocument = await Navigator.pushNamed( + enableHeroAnimation: false, + onTap: (document) { + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: document, isLabelClickable: false, ), - ) as DocumentModel?; - if (updatedDocument != document) { - context.read().reload(); - } + ); }, ) ], diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart index dec42e2..3ec4326 100644 --- a/lib/features/document_upload/cubit/document_upload_cubit.dart +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -14,21 +14,17 @@ part 'document_upload_state.dart'; class DocumentUploadCubit extends Cubit { final PaperlessDocumentsApi _documentApi; - final LabelRepository _tagRepository; - final LabelRepository - _correspondentRepository; - final LabelRepository - _documentTypeRepository; + final LabelRepository _tagRepository; + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; final List _subs = []; DocumentUploadCubit({ required PaperlessDocumentsApi documentApi, - required LabelRepository tagRepository, - required LabelRepository - correspondentRepository, - required LabelRepository - documentTypeRepository, + required LabelRepository tagRepository, + required LabelRepository correspondentRepository, + required LabelRepository documentTypeRepository, }) : _documentApi = documentApi, _tagRepository = tagRepository, _correspondentRepository = correspondentRepository, 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 dc91d9e..49dca21 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -8,10 +8,7 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/type/types.dart'; -import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; @@ -20,7 +17,6 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/constants.dart'; class DocumentUploadPreparationPage extends StatefulWidget { final Uint8List fileBytes; @@ -173,9 +169,8 @@ class _DocumentUploadPreparationPageState formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialName) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => + context.read>(), child: AddDocumentTypePage(initialName: initialName), ), textFieldLabel: @@ -189,9 +184,8 @@ class _DocumentUploadPreparationPageState formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialName) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => + context.read>(), child: AddCorrespondentPage(initialName: initialName), ), textFieldLabel: diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index bc7bd04..b251a0f 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'dart:developer'; +import 'package:flutter/foundation.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; -import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -17,14 +17,21 @@ class DocumentsCubit extends HydratedCubit final DocumentChangedNotifier notifier; DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) { - reload(); + notifier.subscribe( + this, + onUpdated: replace, + onDeleted: remove, + ); } - Future bulkRemove(List documents) async { - log("[DocumentsCubit] bulkRemove"); + Future bulkDelete(List documents) async { + debugPrint("[DocumentsCubit] bulkRemove"); await api.bulkAction( BulkDeleteAction(documents.map((doc) => doc.id)), ); + for (final deletedDoc in documents) { + notifier.notifyDeleted(deletedDoc); + } await reload(); } @@ -33,7 +40,7 @@ class DocumentsCubit extends HydratedCubit Iterable addTags = const [], Iterable removeTags = const [], }) async { - log("[DocumentsCubit] bulkEditTags"); + debugPrint("[DocumentsCubit] bulkEditTags"); await api.bulkAction(BulkModifyTagsAction( documents.map((doc) => doc.id), addTags: addTags, @@ -43,7 +50,7 @@ class DocumentsCubit extends HydratedCubit } void toggleDocumentSelection(DocumentModel model) { - log("[DocumentsCubit] toggleSelection"); + debugPrint("[DocumentsCubit] toggleSelection"); if (state.selectedIds.contains(model.id)) { emit( state.copyWith( @@ -58,12 +65,12 @@ class DocumentsCubit extends HydratedCubit } void resetSelection() { - log("[DocumentsCubit] resetSelection"); + debugPrint("[DocumentsCubit] resetSelection"); emit(state.copyWith(selection: [])); } void reset() { - log("[DocumentsCubit] reset"); + debugPrint("[DocumentsCubit] reset"); emit(const DocumentsState()); } @@ -81,4 +88,10 @@ class DocumentsCubit extends HydratedCubit Map? toJson(DocumentsState state) { return state.toJson(); } + + @override + Future close() { + notifier.unsubscribe(this); + return super.close(); + } } diff --git a/lib/features/documents/bloc/documents_state.dart b/lib/features/documents/bloc/documents_state.dart index 1e080a5..371c729 100644 --- a/lib/features/documents/bloc/documents_state.dart +++ b/lib/features/documents/bloc/documents_state.dart @@ -3,7 +3,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; class DocumentsState extends PagedDocumentsState { - @JsonKey(ignore: true) + @JsonKey(includeFromJson: true, includeToJson: false) final List selection; const DocumentsState({ @@ -34,11 +34,8 @@ class DocumentsState extends PagedDocumentsState { @override List get props => [ - hasLoaded, - filter, - value, selection, - isLoading, + ...super.props, ]; Map toJson() { diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 9238a3e..b9b8c80 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -160,8 +160,7 @@ class _DocumentEditPageState extends State { notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialValue) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: AddStoragePathPage(initalValue: initialValue), ), textFieldLabel: S.of(context).documentStoragePathPropertyLabel, @@ -182,8 +181,7 @@ class _DocumentEditPageState extends State { notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialValue) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: AddCorrespondentPage(initialName: initialValue), ), textFieldLabel: S.of(context).documentCorrespondentPropertyLabel, @@ -215,8 +213,7 @@ class _DocumentEditPageState extends State { notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (currentInput) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: AddDocumentTypePage( initialName: currentInput, ), diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 0889c9a..6074712 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -249,7 +249,7 @@ class _DocumentsPageState extends State Builder( builder: (context) { return RefreshIndicator( - edgeOffset: kToolbarHeight, + edgeOffset: kToolbarHeight + kTextTabBarHeight, onRefresh: _onReloadDocuments, notificationPredicate: (_) => connectivityState.isConnected, @@ -263,13 +263,14 @@ class _DocumentsPageState extends State ), _buildViewActions(), BlocBuilder( - buildWhen: (previous, current) => - !const ListEquality().equals( - previous.documents, - current.documents, - ) || - previous.selectedIds != - current.selectedIds, + // Not required anymore since saved views are now handled separately + // buildWhen: (previous, current) => + // !const ListEquality().equals( + // previous.documents, + // current.documents, + // ) || + // previous.selectedIds != + // current.selectedIds, builder: (context, state) { if (state.hasLoaded && state.documents.isEmpty) { @@ -323,7 +324,7 @@ class _DocumentsPageState extends State Builder( builder: (context) { return RefreshIndicator( - edgeOffset: kToolbarHeight, + edgeOffset: kToolbarHeight + kTextTabBarHeight, onRefresh: _onReloadSavedViews, notificationPredicate: (_) => connectivityState.isConnected, @@ -390,7 +391,7 @@ class _DocumentsPageState extends State try { await context .read() - .bulkRemove(documentsState.selection); + .bulkDelete(documentsState.selection); showSnackBar( context, S.of(context).documentsPageBulkDeleteSuccessfulText, @@ -467,20 +468,14 @@ class _DocumentsPageState extends State } } - Future _openDetails(DocumentModel document) async { - final updatedModel = await Navigator.pushNamed( + void _openDetails(DocumentModel document) { + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: document, ), - ) as DocumentModel?; - // final updatedModel = await Navigator.of(context).push( - // _buildDetailsPageRoute(document), - // ); - if (updatedModel != document) { - context.read().reload(); - } + ); } void _addTagToFilter(int tagId) { diff --git a/lib/features/documents/view/widgets/adaptive_documents_view.dart b/lib/features/documents/view/widgets/adaptive_documents_view.dart index ee1d343..eb997a5 100644 --- a/lib/features/documents/view/widgets/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/adaptive_documents_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/document_grid_loading_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; @@ -23,6 +23,7 @@ abstract class AdaptiveDocumentsView extends StatelessWidget { final void Function(int? id)? onDocumentTypeSelected; final void Function(int? id)? onStoragePathSelected; + bool get showLoadingPlaceholder => (!hasLoaded && isLoading); const AdaptiveDocumentsView({ super.key, this.selectedDocumentIds = const [], @@ -56,6 +57,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { super.onTap, super.selectedDocumentIds, super.viewType, + super.enableHeroAnimation, required super.isLoading, required super.hasLoaded, }); @@ -71,8 +73,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { } Widget _buildListView() { - if (!hasLoaded && isLoading) { - return const DocumentsListLoadingWidget(); + if (showLoadingPlaceholder) { + return DocumentsListLoadingWidget.sliver(); } return SliverList( delegate: SliverChildBuilderDelegate( @@ -91,6 +93,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { onCorrespondentSelected: onCorrespondentSelected, onDocumentTypeSelected: onDocumentTypeSelected, onStoragePathSelected: onStoragePathSelected, + enableHeroAnimation: enableHeroAnimation, ), ); }, @@ -99,8 +102,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { } Widget _buildGridView() { - if (!hasLoaded && isLoading) { - return const DocumentsListLoadingWidget(); + if (showLoadingPlaceholder) { + return DocumentGridLoadingWidget.sliver(); } return SliverGrid.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -162,10 +165,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { } Widget _buildListView() { - if (!hasLoaded && isLoading) { - return const CustomScrollView(slivers: [ - DocumentsListLoadingWidget(), - ]); + if (showLoadingPlaceholder) { + return DocumentsListLoadingWidget(); } return ListView.builder( @@ -194,12 +195,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { } Widget _buildGridView() { - if (!hasLoaded && isLoading) { - return const CustomScrollView( - slivers: [ - DocumentsListLoadingWidget(), - ], - ); //TODO: Build grid skeleton + if (showLoadingPlaceholder) { + return DocumentGridLoadingWidget(); } return GridView.builder( controller: scrollController, diff --git a/lib/features/documents/view/widgets/document_grid_loading_widget.dart b/lib/features/documents/view/widgets/document_grid_loading_widget.dart index e69de29..d18d5d9 100644 --- a/lib/features/documents/view/widgets/document_grid_loading_widget.dart +++ b/lib/features/documents/view/widgets/document_grid_loading_widget.dart @@ -0,0 +1,102 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; +import 'package:shimmer/shimmer.dart'; + +class DocumentGridLoadingWidget extends StatelessWidget + with DocumentItemPlaceholder { + final bool _isSliver; + @override + final Random random = Random(1257195195); + DocumentGridLoadingWidget({super.key}) : _isSliver = false; + + DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true; + + @override + Widget build(BuildContext context) { + const delegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + childAspectRatio: 1 / 2, + ); + if (_isSliver) { + return SliverGrid.builder( + gridDelegate: delegate, + itemBuilder: (context, index) => _buildPlaceholderGridItem(context), + ); + } + return GridView.builder( + gridDelegate: delegate, + itemBuilder: (context, index) => _buildPlaceholderGridItem(context), + ); + } + + Widget _buildPlaceholderGridItem(BuildContext context) { + final values = nextValues; + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + elevation: 1.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShimmerPlaceholder( + child: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Container( + color: Colors.white, + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ShimmerPlaceholder( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextPlaceholder( + length: values.correspondentLength, + fontSize: 16, + ).padded(1), + TextPlaceholder( + length: values.titleLength, + fontSize: 16, + ), + if (values.tagCount > 0) ...[ + const Spacer(), + TagsPlaceholder( + count: values.tagCount, + dense: true, + ), + ], + const Spacer(), + TextPlaceholder( + length: 100, + fontSize: + Theme.of(context).textTheme.bodySmall!.fontSize!, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/documents/view/widgets/documents_list_loading_widget.dart b/lib/features/documents/view/widgets/documents_list_loading_widget.dart index 433a607..034c074 100644 --- a/lib/features/documents/view/widgets/documents_list_loading_widget.dart +++ b/lib/features/documents/view/widgets/documents_list_loading_widget.dart @@ -1,42 +1,42 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:shimmer/shimmer.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; -class DocumentsListLoadingWidget extends StatelessWidget { - static const _tags = [" ", " ", " "]; - static const _titleLengths = [double.infinity, 150.0, 200.0]; - static const _correspondentLengths = [200.0, 300.0, 150.0]; - static const _fontSize = 16.0; +class DocumentsListLoadingWidget extends StatelessWidget + with DocumentItemPlaceholder { + final bool _isSliver; + DocumentsListLoadingWidget({super.key}) : _isSliver = false; - const DocumentsListLoadingWidget({super.key - }); + DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true; + + @override + final Random random = Random(1209571050); @override Widget build(BuildContext context) { - final _random = Random(); - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return _buildFakeListItem(context, _random); - }, - ), - ); + if (_isSliver) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => _buildFakeListItem(context), + ), + ); + } else { + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildFakeListItem(context), + ); + } } - Widget _buildFakeListItem(BuildContext context, Random random) { - final tagCount = random.nextInt(_tags.length + 1); - final correspondentLength = - _correspondentLengths[random.nextInt(_correspondentLengths.length - 1)]; - final titleLength = _titleLengths[random.nextInt(_titleLengths.length - 1)]; - return Shimmer.fromColors( - baseColor: Theme.of(context).brightness == Brightness.light - ? Colors.grey[300]! - : Colors.grey[900]!, - highlightColor: Theme.of(context).brightness == Brightness.light - ? Colors.grey[100]! - : Colors.grey[600]!, + Widget _buildFakeListItem(BuildContext context) { + const fontSize = 14.0; + final values = nextValues; + return ShimmerPlaceholder( child: ListTile( contentPadding: const EdgeInsets.all(8), dense: true, @@ -45,15 +45,17 @@ class DocumentsListLoadingWidget extends StatelessWidget { borderRadius: BorderRadius.circular(8), child: Container( color: Colors.white, - height: 50, + height: double.infinity, width: 35, ), ), - title: Container( - padding: const EdgeInsets.symmetric(vertical: 2.0), - width: correspondentLength, - height: _fontSize, - color: Colors.white, + title: Row( + children: [ + TextPlaceholder( + length: values.correspondentLength, + fontSize: fontSize, + ), + ], ), subtitle: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), @@ -61,21 +63,16 @@ class DocumentsListLoadingWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 2.0), - height: _fontSize, - width: titleLength, - color: Colors.white, + TextPlaceholder( + length: values.titleLength, + fontSize: fontSize, + ), + if (values.tagCount > 0) + TagsPlaceholder(count: values.tagCount, dense: true), + TextPlaceholder( + length: 100, + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!, ), - Wrap( - spacing: 2.0, - children: List.generate( - tagCount, - (index) => InputChip( - label: Text(_tags[random.nextInt(_tags.length)]), - ), - ), - ).paddedOnly(top: 4), ], ), ), diff --git a/lib/features/documents/view/widgets/items/document_list_item.dart b/lib/features/documents/view/widgets/items/document_list_item.dart index e76b802..6732d38 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -56,7 +56,7 @@ class DocumentListItem extends DocumentItem { Text( document.title, overflow: TextOverflow.ellipsis, - maxLines: document.tags.isEmpty ? 2 : 1, + maxLines: 1, ), AbsorbPointer( absorbing: isSelectionActive, diff --git a/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart b/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart new file mode 100644 index 0000000..951e5ff --- /dev/null +++ b/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart @@ -0,0 +1,30 @@ +import 'dart:math'; + +mixin DocumentItemPlaceholder { + static const _tags = [" ", " ", " "]; + static const _titleLengths = [double.infinity, 150.0, 200.0]; + static const _correspondentLengths = [120.0, 80.0, 40.0]; + + Random get random; + + RandomDocumentItemPlaceholderValues get nextValues { + return RandomDocumentItemPlaceholderValues( + tagCount: random.nextInt(_tags.length + 1), + correspondentLength: _correspondentLengths[ + random.nextInt(_correspondentLengths.length - 1)], + titleLength: _titleLengths[random.nextInt(_titleLengths.length - 1)], + ); + } +} + +class RandomDocumentItemPlaceholderValues { + final int tagCount; + final double correspondentLength; + final double titleLength; + + RandomDocumentItemPlaceholderValues({ + required this.tagCount, + required this.correspondentLength, + required this.titleLength, + }); +} diff --git a/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart b/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart new file mode 100644 index 0000000..757f3ef --- /dev/null +++ b/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class TagsPlaceholder extends StatelessWidget { + static const _lengths = [24, 36, 16, 48]; + final int count; + final bool dense; + const TagsPlaceholder({ + super.key, + required this.count, + required this.dense, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + child: ListView.separated( + itemCount: count, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) => FilterChip( + labelPadding: + dense ? const EdgeInsets.symmetric(horizontal: 2) : null, + padding: dense ? const EdgeInsets.all(4) : null, + visualDensity: const VisualDensity(vertical: -2), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: BorderSide.none, + onSelected: (_) {}, + selected: false, + label: Text( + List.filled(_lengths[index], " ").join(), + ), + ), + separatorBuilder: (context, _) => const SizedBox(width: 4), + ), + ); + } +} diff --git a/lib/features/documents/view/widgets/placeholder/text_placeholder.dart b/lib/features/documents/view/widgets/placeholder/text_placeholder.dart new file mode 100644 index 0000000..ef02729 --- /dev/null +++ b/lib/features/documents/view/widgets/placeholder/text_placeholder.dart @@ -0,0 +1,26 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; + +class TextPlaceholder extends StatelessWidget { + final double length; + final double fontSize; + + const TextPlaceholder({ + super.key, + required this.length, + required this.fontSize, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + width: length, + height: fontSize, + ); + } +} diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index a4610a8..d87466b 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -44,16 +44,12 @@ class SortDocumentsButton extends StatelessWidget { providers: [ BlocProvider( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), BlocProvider( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), ], diff --git a/lib/features/edit_document/cubit/edit_document_cubit.dart b/lib/features/edit_document/cubit/edit_document_cubit.dart index 85be2d1..6942d0b 100644 --- a/lib/features/edit_document/cubit/edit_document_cubit.dart +++ b/lib/features/edit_document/cubit/edit_document_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:collection/collection.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; @@ -16,31 +17,28 @@ class EditDocumentCubit extends Cubit { final DocumentModel _initialDocument; final PaperlessDocumentsApi _docsApi; - final LabelRepository - _correspondentRepository; - final LabelRepository - _documentTypeRepository; - final LabelRepository - _storagePathRepository; - final LabelRepository _tagRepository; - + final DocumentChangedNotifier _notifier; + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; + final LabelRepository _storagePathRepository; + final LabelRepository _tagRepository; final List _subscriptions = []; + EditDocumentCubit( DocumentModel document, { required PaperlessDocumentsApi documentsApi, - required LabelRepository - correspondentRepository, - required LabelRepository - documentTypeRepository, - required LabelRepository - storagePathRepository, - required LabelRepository tagRepository, + required LabelRepository correspondentRepository, + required LabelRepository documentTypeRepository, + required LabelRepository storagePathRepository, + required LabelRepository tagRepository, + required DocumentChangedNotifier notifier, }) : _initialDocument = document, _docsApi = documentsApi, _correspondentRepository = correspondentRepository, _documentTypeRepository = documentTypeRepository, _storagePathRepository = storagePathRepository, _tagRepository = tagRepository, + _notifier = notifier, super( EditDocumentState( document: document, @@ -50,6 +48,7 @@ class EditDocumentCubit extends Cubit { tags: tagRepository.current?.values ?? {}, ), ) { + _notifier.subscribe(this, onUpdated: replace); _subscriptions.add( _correspondentRepository.values .listen((v) => emit(state.copyWith(correspondents: v?.values))), @@ -71,6 +70,8 @@ class EditDocumentCubit extends Cubit { Future updateDocument(DocumentModel document) async { final updated = await _docsApi.update(document); + _notifier.notifyUpdated(updated); + // Reload changed labels (documentCount property changes with removal/add) if (document.documentType != _initialDocument.documentType) { _documentTypeRepository @@ -88,7 +89,10 @@ class EditDocumentCubit extends Cubit { .equals(document.tags, _initialDocument.tags)) { _tagRepository.findAll(document.tags); } - emit(state.copyWith(document: updated)); + } + + void replace(DocumentModel document) { + emit(state.copyWith(document: document)); } @override @@ -96,6 +100,7 @@ class EditDocumentCubit extends Cubit { for (final sub in _subscriptions) { sub.cancel(); } + _notifier.unsubscribe(this); return super.close(); } } diff --git a/lib/features/edit_label/cubit/edit_label_cubit.dart b/lib/features/edit_label/cubit/edit_label_cubit.dart index 248ca9d..9ec07e8 100644 --- a/lib/features/edit_label/cubit/edit_label_cubit.dart +++ b/lib/features/edit_label/cubit/edit_label_cubit.dart @@ -3,15 +3,15 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart'; class EditLabelCubit extends Cubit> { - final LabelRepository>> _repository; + final LabelRepository _repository; StreamSubscription? _subscription; - EditLabelCubit(LabelRepository>> repository) + EditLabelCubit(LabelRepository repository) : _repository = repository, super(const EditLabelInitial()) { _subscription = repository.values.listen( diff --git a/lib/features/edit_label/view/add_label_page.dart b/lib/features/edit_label/view/add_label_page.dart index 227e032..5b0e593 100644 --- a/lib/features/edit_label/view/add_label_page.dart +++ b/lib/features/edit_label/view/add_label_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -25,8 +25,7 @@ class AddLabelPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>>>(), + context.read>(), ), child: AddLabelFormWidget( pageTitle: pageTitle, diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index 28d2273..8fe7d27 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; +import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -28,8 +28,7 @@ class EditLabelPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>>>(), + context.read>(), ), child: EditLabelForm( label: label, diff --git a/lib/features/edit_label/view/impl/add_correspondent_page.dart b/lib/features/edit_label/view/impl/add_correspondent_page.dart index 9df0cd4..08d4c77 100644 --- a/lib/features/edit_label/view/impl/add_correspondent_page.dart +++ b/lib/features/edit_label/view/impl/add_correspondent_page.dart @@ -15,8 +15,7 @@ class AddCorrespondentPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), child: AddLabelPage( pageTitle: Text(S.of(context).addCorrespondentPageTitle), diff --git a/lib/features/edit_label/view/impl/add_document_type_page.dart b/lib/features/edit_label/view/impl/add_document_type_page.dart index 1fc30ca..e3c19e9 100644 --- a/lib/features/edit_label/view/impl/add_document_type_page.dart +++ b/lib/features/edit_label/view/impl/add_document_type_page.dart @@ -18,8 +18,7 @@ class AddDocumentTypePage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>(), + context.read>(), ), child: AddLabelPage( pageTitle: Text(S.of(context).addDocumentTypePageTitle), diff --git a/lib/features/edit_label/view/impl/add_storage_path_page.dart b/lib/features/edit_label/view/impl/add_storage_path_page.dart index c5926e4..3ab343c 100644 --- a/lib/features/edit_label/view/impl/add_storage_path_page.dart +++ b/lib/features/edit_label/view/impl/add_storage_path_page.dart @@ -16,8 +16,7 @@ class AddStoragePathPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>(), + context.read>(), ), child: AddLabelPage( pageTitle: Text(S.of(context).addStoragePathPageTitle), diff --git a/lib/features/edit_label/view/impl/add_tag_page.dart b/lib/features/edit_label/view/impl/add_tag_page.dart index 157db6a..76257eb 100644 --- a/lib/features/edit_label/view/impl/add_tag_page.dart +++ b/lib/features/edit_label/view/impl/add_tag_page.dart @@ -19,7 +19,7 @@ class AddTagPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context.read>(), + context.read>(), ), child: AddLabelPage( pageTitle: Text(S.of(context).addTagPageTitle), diff --git a/lib/features/edit_label/view/impl/edit_correspondent_page.dart b/lib/features/edit_label/view/impl/edit_correspondent_page.dart index e620db9..5c01408 100644 --- a/lib/features/edit_label/view/impl/edit_correspondent_page.dart +++ b/lib/features/edit_label/view/impl/edit_correspondent_page.dart @@ -14,8 +14,7 @@ class EditCorrespondentPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), child: EditLabelPage( label: correspondent, diff --git a/lib/features/edit_label/view/impl/edit_document_type_page.dart b/lib/features/edit_label/view/impl/edit_document_type_page.dart index a3a7a9b..d698aec 100644 --- a/lib/features/edit_label/view/impl/edit_document_type_page.dart +++ b/lib/features/edit_label/view/impl/edit_document_type_page.dart @@ -14,8 +14,7 @@ class EditDocumentTypePage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>(), + context.read>(), ), child: EditLabelPage( label: documentType, diff --git a/lib/features/edit_label/view/impl/edit_storage_path_page.dart b/lib/features/edit_label/view/impl/edit_storage_path_page.dart index 2994796..73a66e0 100644 --- a/lib/features/edit_label/view/impl/edit_storage_path_page.dart +++ b/lib/features/edit_label/view/impl/edit_storage_path_page.dart @@ -15,8 +15,7 @@ class EditStoragePathPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context - .read>(), + context.read>(), ), child: EditLabelPage( label: storagePath, diff --git a/lib/features/edit_label/view/impl/edit_tag_page.dart b/lib/features/edit_label/view/impl/edit_tag_page.dart index 686873d..678b944 100644 --- a/lib/features/edit_label/view/impl/edit_tag_page.dart +++ b/lib/features/edit_label/view/impl/edit_tag_page.dart @@ -18,7 +18,7 @@ class EditTagPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => EditLabelCubit( - context.read>(), + context.read>(), ), child: EditLabelPage( label: tag, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 0e9729f..c6e9f9f 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -23,6 +23,7 @@ import 'package:paperless_mobile/features/home/view/route_description.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; @@ -59,6 +60,7 @@ class _HomePageState extends State { context.read(), context.read(), context.read(), + context.read(), ); context.read().reload(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { @@ -228,7 +230,23 @@ class _HomePageState extends State { value: _scannerCubit, child: const ScannerPage(), ), - const LabelsPage(), + MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => LabelCubit(context.read()), + ), + BlocProvider( + create: (context) => LabelCubit(context.read()), + ), + BlocProvider( + create: (context) => LabelCubit(context.read()), + ), + BlocProvider( + create: (context) => LabelCubit(context.read()), + ), + ], + child: const LabelsPage(), + ), BlocProvider.value( value: _inboxCubit, child: const InboxPage(), @@ -302,16 +320,10 @@ class _HomePageState extends State { void _initializeData(BuildContext context) { try { - context.read>().findAll(); - context - .read>() - .findAll(); - context - .read>() - .findAll(); - context - .read>() - .findAll(); + context.read>().findAll(); + context.read>().findAll(); + context.read>().findAll(); + context.read>().findAll(); context.read().findAll(); context.read().updateInformtion(); } on PaperlessServerException catch (error, stackTrace) { diff --git a/lib/features/home/view/widget/verify_identity_page.dart b/lib/features/home/view/widget/verify_identity_page.dart index 78814d7..a518d57 100644 --- a/lib/features/home/view/widget/verify_identity_page.dart +++ b/lib/features/home/view/widget/verify_identity_page.dart @@ -70,16 +70,10 @@ class VerifyIdentityPage extends StatelessWidget { void _logout(BuildContext context) { context.read().logout(); - context.read>().clear(); - context - .read>() - .clear(); - context - .read>() - .clear(); - context - .read>() - .clear(); + context.read>().clear(); + context.read>().clear(); + context.read>().clear(); + context.read>().clear(); context.read().clear(); HydratedBloc.storage.clear(); } diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index 8ad1c81..b408f04 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -1,23 +1,20 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; class InboxCubit extends HydratedCubit with PagedDocumentsMixin { - final LabelRepository _tagsRepository; - final LabelRepository - _correspondentRepository; - final LabelRepository - _documentTypeRepository; + final LabelRepository _tagsRepository; + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; final PaperlessDocumentsApi _documentsApi; + @override final DocumentChangedNotifier notifier; @@ -28,7 +25,6 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { @override PaperlessDocumentsApi get api => _documentsApi; - Timer? _taskTimer; InboxCubit( this._tagsRepository, this._documentsApi, @@ -45,11 +41,20 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { availableTags: _tagsRepository.current?.values ?? {}, ), ) { - _subscriptions.addAll( - notifier.listen( - onDeleted: remove, - onUpdated: replace, - ), + notifier.subscribe( + this, + onDeleted: remove, + onUpdated: (document) { + if (document.tags + .toSet() + .intersection(state.inboxTags.toSet()) + .isEmpty) { + remove(document); + emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); + } else { + replace(document); + } + }, ); _subscriptions.add( _tagsRepository.values.listen((event) { @@ -74,21 +79,35 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { } }), ); - //TODO: Do this properly in a background task. - _taskTimer = Timer.periodic(const Duration(seconds: 5), (timer) { + + refreshItemsInInboxCount(false); + loadInbox(); + + Timer.periodic(const Duration(seconds: 5), (timer) { + if (isClosed) { + timer.cancel(); + } refreshItemsInInboxCount(); }); } - void refreshItemsInInboxCount() async { + void refreshItemsInInboxCount([bool shouldLoadInbox = true]) async { final stats = await _statsApi.getServerStatistics(); - emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox)); + + if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) { + loadInbox(); + } + emit( + state.copyWith( + itemsInInboxCount: stats.documentsInInbox, + ), + ); } /// /// Fetches inbox tag ids and loads the inbox items (documents). /// - Future initializeInbox() async { + Future loadInbox() async { final inboxTags = await _tagsRepository.findAll().then( (tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!), ); @@ -104,7 +123,7 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { ); } emit(state.copyWith(inboxTags: inboxTags)); - return updateFilter( + updateFilter( filter: DocumentFilter( sortField: SortField.added, tags: IdsTagsQuery.fromIds(inboxTags), @@ -121,11 +140,12 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { document.tags.toSet().intersection(state.inboxTags.toSet()); final updatedTags = {...document.tags}..removeAll(tagsToRemove); - await api.update( + final updatedDocument = await api.update( document.copyWith(tags: updatedTags), ); - await remove(document); - emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); + // Remove first so document is not replaced first. + remove(document); + notifier.notifyUpdated(updatedDocument); return tagsToRemove; } @@ -136,10 +156,12 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { DocumentModel document, Iterable removedTags, ) async { - final updatedDoc = document.copyWith( - tags: {...document.tags, ...removedTags}, + final updatedDocument = await _documentsApi.update( + document.copyWith( + tags: {...document.tags, ...removedTags}, + ), ); - await _documentsApi.update(updatedDoc); + notifier.notifyUpdated(updatedDocument); emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1)); return reload(); } @@ -166,22 +188,12 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { } } - void replaceUpdatedDocument(DocumentModel document) { - if (document.tags.any((id) => state.inboxTags.contains(id))) { - // If replaced document still has inbox tag assigned: - replace(document); - } else { - // Remove document from inbox. - remove(document); - emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); - } - } - Future assignAsn(DocumentModel document) async { if (document.archiveSerialNumber == null) { final int asn = await _documentsApi.findNextAsn(); final updatedDocument = await _documentsApi .update(document.copyWith(archiveSerialNumber: asn)); + replace(updatedDocument); } } @@ -202,7 +214,6 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { @override Future close() { - _taskTimer?.cancel(); for (var sub in _subscriptions) { sub.cancel(); } diff --git a/lib/features/inbox/bloc/state/inbox_state.dart b/lib/features/inbox/bloc/state/inbox_state.dart index a2a5814..8546bac 100644 --- a/lib/features/inbox/bloc/state/inbox_state.dart +++ b/lib/features/inbox/bloc/state/inbox_state.dart @@ -4,9 +4,7 @@ import 'package:paperless_mobile/features/paged_document_view/model/paged_docume part 'inbox_state.g.dart'; -@JsonSerializable( - ignoreUnannotated: true, -) +@JsonSerializable(ignoreUnannotated: true) class InboxState extends PagedDocumentsState { final Iterable inboxTags; diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 11cb7d2..92fbc1d 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -31,7 +31,7 @@ class _InboxPageState extends State { @override void initState() { super.initState(); - context.read().initializeInbox(); + context.read().loadInbox(); _scrollController.addListener(_listenForLoadNewData); } @@ -57,6 +57,12 @@ class _InboxPageState extends State { @override Widget build(BuildContext context) { + final safeAreaPadding = MediaQuery.of(context).padding; + final availableHeight = MediaQuery.of(context).size.height - + kToolbarHeight - + kBottomNavigationBarHeight - + safeAreaPadding.top - + safeAreaPadding.bottom; return Scaffold( drawer: const AppDrawer(), floatingActionButton: BlocBuilder( @@ -76,97 +82,105 @@ class _InboxPageState extends State { ); }, ), - body: RefreshIndicator( - edgeOffset: 78, - onRefresh: () => context.read().initializeInbox(), - child: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SearchAppBar( - hintText: S.of(context).documentSearchSearchDocuments, - onOpenSearch: showDocumentSearchPage, - ), - ], - body: BlocBuilder( - builder: (context, state) { - if (!state.hasLoaded) { - return const CustomScrollView( - physics: NeverScrollableScrollPhysics(), - slivers: [DocumentsListLoadingWidget()], - ); - } - - if (state.documents.isEmpty) { - return InboxEmptyWidget( - emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, - ); - } - - // Build a list of slivers alternating between SliverToBoxAdapter - // (group header) and a SliverList (inbox items). - final List slivers = _groupByDate(state.documents) - .entries - .map( - (entry) => [ - SliverToBoxAdapter( - child: Align( - alignment: Alignment.centerLeft, - child: ClipRRect( - borderRadius: BorderRadius.circular(32.0), - child: Text( - entry.key, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ).padded(), - ), - ).paddedOnly(top: 8.0), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: entry.value.length, - (context, index) { - if (index < entry.value.length - 1) { - return Column( - children: [ - _buildListItem( - entry.value[index], - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ], - ); - } - return _buildListItem( - entry.value[index], - ); - }, + body: BlocBuilder( + builder: (context, state) { + return SafeArea( + top: true, + child: Builder( + builder: (context) { + // Build a list of slivers alternating between SliverToBoxAdapter + // (group header) and a SliverList (inbox items). + final List slivers = _groupByDate(state.documents) + .entries + .map( + (entry) => [ + SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerLeft, + child: ClipRRect( + borderRadius: BorderRadius.circular(32.0), + child: Text( + entry.key, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ).padded(), + ), + ).paddedOnly(top: 8.0), ), - ), - ], - ) - .flattened - .toList() - ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); - // edgeOffset: kToolbarHeight, + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: entry.value.length, + (context, index) { + if (index < entry.value.length - 1) { + return Column( + children: [ + _buildListItem( + entry.value[index], + ), + const Divider( + indent: 16, + endIndent: 16, + ), + ], + ); + } + return _buildListItem( + entry.value[index], + ); + }, + ), + ), + ], + ) + .flattened + .toList() + ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); + // edgeOffset: kToolbarHeight, - return CustomScrollView( - controller: _scrollController, - slivers: [ - SliverToBoxAdapter( - child: HintCard( - show: !state.isHintAcknowledged, - hintText: S.of(context).inboxPageUsageHintText, - onHintAcknowledged: () => - context.read().acknowledgeHint(), - ), + return RefreshIndicator( + edgeOffset: kToolbarHeight, + onRefresh: context.read().reload, + child: CustomScrollView( + physics: state.documents.isEmpty + ? const NeverScrollableScrollPhysics() + : const AlwaysScrollableScrollPhysics(), + controller: _scrollController, + slivers: [ + SearchAppBar( + hintText: S.of(context).documentSearchSearchDocuments, + onOpenSearch: showDocumentSearchPage, + ), + if (state.documents.isEmpty) + SliverToBoxAdapter( + child: SizedBox( + height: availableHeight, + child: Center( + child: InboxEmptyWidget( + emptyStateRefreshIndicatorKey: + _emptyStateRefreshIndicatorKey, + ), + ), + ), + ) + else if (!state.hasLoaded) + DocumentsListLoadingWidget() + else + SliverToBoxAdapter( + child: HintCard( + show: !state.isHintAcknowledged, + hintText: S.of(context).inboxPageUsageHintText, + onHintAcknowledged: () => + context.read().acknowledgeHint(), + ), + ), + ...slivers, + ], ), - ...slivers, - ], - ); - }, - ), - ), + ); + }, + ), + ); + }, ), ); } @@ -191,12 +205,7 @@ class _InboxPageState extends State { ).padded(), confirmDismiss: (_) => _onItemDismissed(doc), key: UniqueKey(), - child: InboxItem( - document: doc, - onDocumentUpdated: (document) { - context.read().replaceUpdatedDocument(document); - }, - ), + child: InboxItem(document: doc), ); } diff --git a/lib/features/inbox/view/widgets/inbox_empty_widget.dart b/lib/features/inbox/view/widgets/inbox_empty_widget.dart index bf79d2a..b23fce3 100644 --- a/lib/features/inbox/view/widgets/inbox_empty_widget.dart +++ b/lib/features/inbox/view/widgets/inbox_empty_widget.dart @@ -16,7 +16,7 @@ class InboxEmptyWidget extends StatelessWidget { Widget build(BuildContext context) { return RefreshIndicator( key: _emptyStateRefreshIndicatorKey, - onRefresh: () => context.read().initializeInbox(), + onRefresh: () => context.read().loadInbox(), child: Center( child: Column( mainAxisSize: MainAxisSize.max, diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 0f5709e..59b4cab 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -1,13 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; -import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; @@ -19,12 +14,10 @@ import 'package:paperless_mobile/routes/document_details_route.dart'; class InboxItem extends StatefulWidget { static const _a4AspectRatio = 1 / 1.4142; - final void Function(DocumentModel model) onDocumentUpdated; final DocumentModel document; const InboxItem({ super.key, required this.document, - required this.onDocumentUpdated, }); @override @@ -41,17 +34,14 @@ class _InboxItemState extends State { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { - final updatedDocument = await Navigator.pushNamed( + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: widget.document, isLabelClickable: false, ), - ) as DocumentModel?; - if (updatedDocument != null) { - widget.onDocumentUpdated(updatedDocument); - } + ); }, child: SizedBox( height: 200, @@ -104,12 +94,12 @@ class _InboxItemState extends State { ); final actions = [ _buildAssignAsnAction(chipShape, context), - const SizedBox(width: 4.0), + const SizedBox(width: 8.0), ColoredChipWrapper( child: ActionChip( avatar: const Icon(Icons.delete_outline), shape: chipShape, - label: const Text("Delete document"), + label: Text(S.of(context).inboxActionDeleteDocument), onPressed: () async { final shouldDelete = await showDialog( context: context, @@ -124,6 +114,7 @@ class _InboxItemState extends State { ), ), ]; + // return FutureBuilder( // future: _fieldSuggestions, // builder: (context, snapshot) { @@ -151,12 +142,14 @@ class _InboxItemState extends State { mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.bolt_outlined), - SizedBox( - width: 40, + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 50, + ), child: Text( S.of(context).inboxPageQuickActionsLabel, textAlign: TextAlign.center, - maxLines: 2, + maxLines: 3, style: Theme.of(context).textTheme.labelSmall, ), ), @@ -199,7 +192,7 @@ class _InboxItemState extends State { ? Text( '${S.of(context).documentArchiveSerialNumberPropertyShortLabel} #${widget.document.archiveSerialNumber}', ) - : const Text("Assign ASN"), + : Text(S.of(context).inboxActionAssignAsn), onPressed: !hasAsn ? () { setState(() { @@ -233,7 +226,7 @@ class _InboxItemState extends State { Icons.description_outlined, size: Theme.of(context).textTheme.bodyMedium?.fontSize, ), - LabelText( + LabelText( id: widget.document.documentType, style: Theme.of(context).textTheme.bodyMedium, placeholder: "-", @@ -247,7 +240,7 @@ class _InboxItemState extends State { Icons.person_outline, size: Theme.of(context).textTheme.bodyMedium?.fontSize, ), - LabelText( + LabelText( id: widget.document.correspondent, style: Theme.of(context).textTheme.bodyMedium, placeholder: "-", diff --git a/lib/features/labels/bloc/label_cubit.dart b/lib/features/labels/bloc/label_cubit.dart index 383900b..e1136b7 100644 --- a/lib/features/labels/bloc/label_cubit.dart +++ b/lib/features/labels/bloc/label_cubit.dart @@ -3,15 +3,14 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; class LabelCubit extends Cubit> { - final LabelRepository _repository; + final LabelRepository _repository; late StreamSubscription _subscription; - LabelCubit(LabelRepository repository) + LabelCubit(LabelRepository repository) : _repository = repository, super(LabelState( isLoaded: repository.isInitialized, @@ -22,7 +21,8 @@ class LabelCubit extends Cubit> { if (event == null) { emit(LabelState()); } - emit(LabelState(isLoaded: true, labels: event!.values)); + emit( + LabelState(isLoaded: event!.hasLoaded, labels: event.values ?? {})); }, ); } diff --git a/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart b/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart index 7a5e9d0..ffbf773 100644 --- a/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart @@ -7,14 +7,16 @@ import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; class CorrespondentBlocProvider extends StatelessWidget { final Widget child; - const CorrespondentBlocProvider({super.key, required this.child}); + const CorrespondentBlocProvider({ + super.key, + required this.child, + }); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), child: child, ); diff --git a/lib/features/labels/bloc/providers/document_type_bloc_provider.dart b/lib/features/labels/bloc/providers/document_type_bloc_provider.dart index 3aa129f..6ebcd14 100644 --- a/lib/features/labels/bloc/providers/document_type_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/document_type_bloc_provider.dart @@ -13,8 +13,7 @@ class DocumentTypeBlocProvider extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context - .read>(), + context.read>(), ), child: child, ); diff --git a/lib/features/labels/bloc/providers/labels_bloc_provider.dart b/lib/features/labels/bloc/providers/labels_bloc_provider.dart index 1ec58c2..d4a90e2 100644 --- a/lib/features/labels/bloc/providers/labels_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/labels_bloc_provider.dart @@ -18,25 +18,22 @@ class LabelsBlocProvider extends StatelessWidget { providers: [ BlocProvider>( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), BlocProvider>( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), BlocProvider>( create: (context) => LabelCubit( - context.read< - LabelRepository>(), + context.read>(), ), ), BlocProvider>( create: (context) => LabelCubit( - context.read>(), + context.read>(), ), ), ], diff --git a/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart b/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart index 1c03ee4..646c6ad 100644 --- a/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart @@ -13,8 +13,7 @@ class StoragePathBlocProvider extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context - .read>(), + context.read>(), ), child: child, ); diff --git a/lib/features/labels/bloc/providers/tag_bloc_provider.dart b/lib/features/labels/bloc/providers/tag_bloc_provider.dart index fc36546..4368075 100644 --- a/lib/features/labels/bloc/providers/tag_bloc_provider.dart +++ b/lib/features/labels/bloc/providers/tag_bloc_provider.dart @@ -13,7 +13,7 @@ class TagBlocProvider extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context.read>(), + context.read>(), ), child: child, ); diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index 3ea2ae4..0244dad 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -241,8 +241,7 @@ class _TagFormFieldState extends State { final Tag? tag = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => - context.read>(), + create: (context) => context.read>(), child: AddTagPage(initialValue: _textEditingController.text), ), ), diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index d64521c..1e5986d 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -82,7 +82,7 @@ class _LabelsPageState extends State context, ), sliver: SearchAppBar( - hintText: "Search documents", //TODO: INTL + hintText: S.of(context).documentSearchSearchDocuments, onOpenSearch: showDocumentSearchPage, bottom: TabBar( controller: _tabController, @@ -141,176 +141,138 @@ class _LabelsPageState extends State } return true; }, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), + child: RefreshIndicator( + edgeOffset: kToolbarHeight + kTextTabBarHeight, + notificationPredicate: (notification) => + connectedState.isConnected, + onRefresh: () => [ + context.read>(), + context.read>(), + context.read>(), + context.read>(), + ][_currentIndex] + .reload(), + child: TabBarView( + controller: _tabController, + children: [ + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + correspondent: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditCorrespondentPage, + emptyStateActionButtonLabel: S + .of(context) + .labelsPageCorrespondentEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageCorrespondentEmptyStateDescriptionText, + onAddNew: _openAddCorrespondentPage, + ), + ], + ); + }, ), - ), - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + documentType: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditDocumentTypePage, + emptyStateActionButtonLabel: S + .of(context) + .labelsPageDocumentTypeEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageDocumentTypeEmptyStateDescriptionText, + onAddNew: _openAddDocumentTypePage, + ), + ], + ); + }, ), - ), - BlocProvider( - create: (context) => LabelCubit( - context - .read>(), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + tags: IdsTagsQuery.fromIds([label.id!]), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditTagPage, + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag ?? false + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, + ), + emptyStateActionButtonLabel: S + .of(context) + .labelsPageTagsEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageTagsEmptyStateDescriptionText, + onAddNew: _openAddTagPage, + ), + ], + ); + }, ), - ), - BlocProvider( - create: (context) => LabelCubit( - context.read< - LabelRepository>(), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView + .sliverOverlapAbsorberHandleFor(context), + ), + LabelTabView( + onEdit: _openEditStoragePathPage, + filterBuilder: (label) => DocumentFilter( + storagePath: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + contentBuilder: (path) => Text(path.path ?? ""), + emptyStateActionButtonLabel: S + .of(context) + .labelsPageStoragePathEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageStoragePathEmptyStateDescriptionText, + onAddNew: _openAddStoragePathPage, + ), + ], + ); + }, ), - ), - ], - child: RefreshIndicator( - edgeOffset: kToolbarHeight, - notificationPredicate: (notification) => - connectedState.isConnected, - onRefresh: () => [ - context.read>(), - context.read>(), - context.read>(), - context.read>(), - ][_currentIndex] - .reload(), - child: TabBarView( - controller: _tabController, - children: [ - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - LabelTabView( - filterBuilder: (label) => DocumentFilter( - correspondent: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditCorrespondentPage, - emptyStateActionButtonLabel: S - .of(context) - .labelsPageCorrespondentEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageCorrespondentEmptyStateDescriptionText, - onAddNew: _openAddCorrespondentPage, - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - DocumentTypeBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - documentType: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditDocumentTypePage, - emptyStateActionButtonLabel: S - .of(context) - .labelsPageDocumentTypeEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageDocumentTypeEmptyStateDescriptionText, - onAddNew: _openAddDocumentTypePage, - ), - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - TagBlocProvider( - child: LabelTabView( - filterBuilder: (label) => DocumentFilter( - tags: IdsTagsQuery.fromIds([label.id!]), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar( - backgroundColor: t.color, - child: t.isInboxTag ?? false - ? Icon( - Icons.inbox, - color: t.textColor, - ) - : null, - ), - emptyStateActionButtonLabel: S - .of(context) - .labelsPageTagsEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageTagsEmptyStateDescriptionText, - onAddNew: _openAddTagPage, - ), - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - StoragePathBlocProvider( - child: LabelTabView( - onEdit: _openEditStoragePathPage, - filterBuilder: (label) => DocumentFilter( - storagePath: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - contentBuilder: (path) => - Text(path.path ?? ""), - emptyStateActionButtonLabel: S - .of(context) - .labelsPageStoragePathEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageStoragePathEmptyStateDescriptionText, - onAddNew: _openAddStoragePathPage, - ), - ), - ], - ); - }, - ), - ], - ), + ], ), ), ), @@ -326,8 +288,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: EditCorrespondentPage(correspondent: correspondent), ), ), @@ -339,8 +300,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: EditDocumentTypePage(documentType: docType), ), ), @@ -352,8 +312,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => - context.read>(), + create: (context) => context.read>(), child: EditTagPage(tag: tag), ), ), @@ -365,8 +324,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context - .read>(), + create: (context) => context.read>(), child: EditStoragePathPage( storagePath: path, ), @@ -380,8 +338,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: const AddCorrespondentPage(), ), ), @@ -393,8 +350,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context.read< - LabelRepository>(), + create: (context) => context.read>(), child: const AddDocumentTypePage(), ), ), @@ -406,8 +362,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => - context.read>(), + create: (context) => context.read>(), child: const AddTagPage(), ), ), @@ -419,8 +374,7 @@ class _LabelsPageState extends State context, MaterialPageRoute( builder: (_) => RepositoryProvider( - create: (context) => context - .read>(), + create: (context) => context.read>(), child: const AddStoragePathPage(), ), ), diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index 4fc6cb0..69d7285 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -48,8 +48,9 @@ class LabelItem extends StatelessWidget { MaterialPageRoute( builder: (context) => BlocProvider( create: (context) => LinkedDocumentsCubit( - context.read(), filter, + context.read(), + context.read(), ), child: const LinkedDocumentsPage(), ), diff --git a/lib/features/labels/view/widgets/label_tab_view.dart b/lib/features/labels/view/widgets/label_tab_view.dart index 8aecad4..45c68c2 100644 --- a/lib/features/labels/view/widgets/label_tab_view.dart +++ b/lib/features/labels/view/widgets/label_tab_view.dart @@ -37,60 +37,65 @@ class LabelTabView extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, connectivityState) { - return BlocBuilder, LabelState>( - builder: (context, state) { - if (!state.isLoaded && !connectivityState.isConnected) { - return const OfflineWidget(); - } - final labels = state.labels.values.toList()..sort(); - if (labels.isEmpty) { - return SliverFillRemaining( - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - emptyStateDescription, - textAlign: TextAlign.center, - ), - TextButton( - onPressed: onAddNew, - child: Text(emptyStateActionButtonLabel), - ), - ].padded(), + return BlocProvider( + create: (context) => LabelCubit( + context.read(), + ), + child: BlocBuilder( + builder: (context, connectivityState) { + return BlocBuilder, LabelState>( + builder: (context, state) { + if (!state.isLoaded && !connectivityState.isConnected) { + return const OfflineWidget(); + } + final labels = state.labels.values.toList()..sort(); + if (labels.isEmpty) { + return SliverFillRemaining( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + emptyStateDescription, + textAlign: TextAlign.center, + ), + TextButton( + onPressed: onAddNew, + child: Text(emptyStateActionButtonLabel), + ), + ].padded(), + ), ), + ); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final l = labels.elementAt(index); + return LabelItem( + name: l.name, + content: contentBuilder?.call(l) ?? + Text( + translateMatchingAlgorithmName( + context, l.matchingAlgorithm) + + ((l.match?.isNotEmpty ?? false) + ? ": ${l.match}" + : ""), + maxLines: 2, + ), + onOpenEditPage: onEdit, + filterBuilder: filterBuilder, + leading: leadingBuilder?.call(l), + label: l, + ); + }, + childCount: labels.length, ), ); - } - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final l = labels.elementAt(index); - return LabelItem( - name: l.name, - content: contentBuilder?.call(l) ?? - Text( - translateMatchingAlgorithmName( - context, l.matchingAlgorithm) + - ((l.match?.isNotEmpty ?? false) - ? ": ${l.match}" - : ""), - maxLines: 2, - ), - onOpenEditPage: onEdit, - filterBuilder: filterBuilder, - leading: leadingBuilder?.call(l), - label: l, - ); - }, - childCount: labels.length, - ), - ); - }, - ); - }, + }, + ); + }, + ), ); } } diff --git a/lib/features/labels/view/widgets/label_text.dart b/lib/features/labels/view/widgets/label_text.dart index 53b25ff..472fd58 100644 --- a/lib/features/labels/view/widgets/label_text.dart +++ b/lib/features/labels/view/widgets/label_text.dart @@ -2,13 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; -class LabelText - extends StatelessWidget { +class LabelText extends StatelessWidget { final int? id; final String placeholder; final TextStyle? style; @@ -24,7 +21,7 @@ class LabelText Widget build(BuildContext context) { return BlocProvider( create: (context) => LabelCubit( - context.read>(), + context.read>(), ), child: BlocBuilder, LabelState>( builder: (context, state) { diff --git a/lib/features/linked_documents/bloc/linked_documents_cubit.dart b/lib/features/linked_documents/bloc/linked_documents_cubit.dart index bd66d05..c28b368 100644 --- a/lib/features/linked_documents/bloc/linked_documents_cubit.dart +++ b/lib/features/linked_documents/bloc/linked_documents_cubit.dart @@ -11,12 +11,27 @@ class LinkedDocumentsCubit extends Cubit @override final DocumentChangedNotifier notifier; - + LinkedDocumentsCubit( - this.api, DocumentFilter filter, + this.api, this.notifier, ) : super(const LinkedDocumentsState()) { updateFilter(filter: filter); + notifier.subscribe( + this, + onUpdated: replace, + onDeleted: remove, + ); + } + + @override + Future update(DocumentModel document) async { + final updated = await api.update(document); + if (!state.filter.matches(updated)) { + remove(document); + } else { + replace(document); + } } } diff --git a/lib/features/linked_documents/view/pages/linked_documents_page.dart b/lib/features/linked_documents/view/pages/linked_documents_page.dart index 7724a01..2a0ed87 100644 --- a/lib/features/linked_documents/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents/view/pages/linked_documents_page.dart @@ -2,11 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; -import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; -import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -60,18 +56,15 @@ class _LinkedDocumentsPageState extends State { isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, - onTap: (document) async { - final updatedDocument = await Navigator.pushNamed( + onTap: (document) { + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: document, isLabelClickable: false, ), - ) as DocumentModel?; - if (updatedDocument != document) { - context.read().reload(); - } + ); }, ); }, diff --git a/lib/features/notifications/services/local_notification_service.dart b/lib/features/notifications/services/local_notification_service.dart index a30893c..2d85814 100644 --- a/lib/features/notifications/services/local_notification_service.dart +++ b/lib/features/notifications/services/local_notification_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/notifications/services/models/notification_payloads/open_created_document_notification_payload.dart'; @@ -121,7 +122,7 @@ class LocalNotificationService { ) {} void onDidReceiveNotificationResponse(NotificationResponse response) { - log("Received Notification: ${response.payload}"); + debugPrint("Received Notification: ${response.payload}"); if (response.notificationResponseType == NotificationResponseType.selectedNotificationAction) { final action = diff --git a/lib/features/paged_document_view/paged_documents_mixin.dart b/lib/features/paged_document_view/paged_documents_mixin.dart index 22bebe1..2800502 100644 --- a/lib/features/paged_document_view/paged_documents_mixin.dart +++ b/lib/features/paged_document_view/paged_documents_mixin.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -73,14 +75,18 @@ mixin PagedDocumentsMixin try { final filter = state.filter.copyWith(page: 1); final result = await api.findAll(filter); - emit(state.copyWithPaged( - hasLoaded: true, - value: [result], - isLoading: false, - filter: filter, - )); + if (!isClosed) { + emit(state.copyWithPaged( + hasLoaded: true, + value: [result], + isLoading: false, + filter: filter, + )); + } } finally { - emit(state.copyWithPaged(isLoading: false)); + if (!isClosed) { + emit(state.copyWithPaged(isLoading: false)); + } } } @@ -88,16 +94,10 @@ mixin PagedDocumentsMixin /// Updates a document. If [shouldReload] is false, the updated document will /// replace the currently loaded one, otherwise all documents will be reloaded. /// - Future update( - DocumentModel document, { - bool shouldReload = true, - }) async { + Future update(DocumentModel document) async { final updatedDocument = await api.update(document); - if (shouldReload) { - await reload(); - } else { - replace(updatedDocument); - } + notifier.notifyUpdated(updatedDocument); + // replace(updatedDocument); } /// @@ -107,7 +107,8 @@ mixin PagedDocumentsMixin emit(state.copyWithPaged(isLoading: true)); try { await api.delete(document); - await remove(document); + notifier.notifyDeleted(document); + // remove(document); // Removing deleted now works with the change notifier. } finally { emit(state.copyWithPaged(isLoading: false)); } @@ -117,7 +118,7 @@ mixin PagedDocumentsMixin /// Removes [document] from the currently loaded state. /// Does not delete it from the server! /// - Future remove(DocumentModel document) async { + void remove(DocumentModel document) { final index = state.value.indexWhere( (page) => page.results.any((element) => element.id == document.id), ); @@ -144,23 +145,36 @@ mixin PagedDocumentsMixin /// /// Replaces the document with the same id as [document] from the currently - /// loaded state. + /// loaded state if the document's properties still match the given filter criteria, otherwise removes it. /// Future replace(DocumentModel document) async { - final index = state.value.indexWhere( + final matchesFilterCriteria = state.filter.matches(document); + if (!matchesFilterCriteria) { + return remove(document); + } + final pageIndex = state.value.indexWhere( (page) => page.results.any((element) => element.id == document.id), ); - if (index != -1) { - final foundPage = state.value[index]; + if (pageIndex != -1) { + final foundPage = state.value[pageIndex]; final replacementPage = foundPage.copyWith( - results: foundPage.results..replaceRange(index, index + 1, [document]), + results: foundPage.results + .map((doc) => doc.id == document.id ? document : doc) + .toList(), ); - emit(state.copyWithPaged( + final newState = state.copyWithPaged( value: state.value .mapIndexed((currIndex, element) => - currIndex == index ? replacementPage : element) + currIndex == pageIndex ? replacementPage : element) .toList(), - )); + ); + emit(newState); } } + + @override + Future close() { + notifier.unsubscribe(this); + return super.close(); + } } diff --git a/lib/features/saved_view/cubit/saved_view_cubit.dart b/lib/features/saved_view/cubit/saved_view_cubit.dart index 2372eb2..6ff9837 100644 --- a/lib/features/saved_view/cubit/saved_view_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_cubit.dart @@ -34,7 +34,9 @@ class SavedViewCubit extends Cubit { Future initialize() async { final views = await _repository.findAll(); final values = {for (var element in views) element.id!: element}; - emit(SavedViewState(value: values, hasLoaded: true)); + if (!isClosed) { + emit(SavedViewState(value: values, hasLoaded: true)); + } } Future reload() => initialize(); diff --git a/lib/features/saved_view/cubit/saved_view_details_cubit.dart b/lib/features/saved_view/cubit/saved_view_details_cubit.dart index 9b2dfd7..b9cec31 100644 --- a/lib/features/saved_view/cubit/saved_view_details_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_details_cubit.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -10,11 +11,20 @@ class SavedViewDetailsCubit extends Cubit @override final PaperlessDocumentsApi api; + @override + final DocumentChangedNotifier notifier; + final SavedView savedView; SavedViewDetailsCubit( - this.api, { + this.api, + this.notifier, { required this.savedView, }) : super(const SavedViewDetailsState()) { + notifier.subscribe( + this, + onDeleted: remove, + onUpdated: replace, + ); updateFilter(filter: savedView.toDocumentFilter()); } } diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart index 2090c3d..e5af476 100644 --- a/lib/features/saved_view/view/saved_view_list.dart +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -42,6 +42,7 @@ class SavedViewList extends StatelessWidget { providers: [ BlocProvider( create: (context) => SavedViewDetailsCubit( + context.read(), context.read(), savedView: view, ), diff --git a/lib/features/saved_view/view/saved_view_page.dart b/lib/features/saved_view/view/saved_view_page.dart index cd3aa0f..969f91e 100644 --- a/lib/features/saved_view/view/saved_view_page.dart +++ b/lib/features/saved_view/view/saved_view_page.dart @@ -117,18 +117,14 @@ class _SavedViewPageState extends State { ); } - void _onOpenDocumentDetails(DocumentModel document) async { - final updatedDocument = await Navigator.pushNamed( + void _onOpenDocumentDetails(DocumentModel document) { + Navigator.pushNamed( context, DocumentDetailsRoute.routeName, arguments: DocumentDetailsRouteArguments( document: document, isLabelClickable: false, ), - ) as DocumentModel?; - if (updatedDocument != document) { - // Reload in case document was edited and might not fulfill filter criteria of saved view anymore - context.read().reload(); - } + ); } } diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 29a3952..3639316 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -12,9 +12,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; @@ -46,6 +43,15 @@ class _ScannerPageState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { + final safeAreaPadding = MediaQuery.of(context).padding; + final availableHeight = MediaQuery.of(context).size.height - + 2 * kToolbarHeight - + kTextTabBarHeight - + kBottomNavigationBarHeight - + safeAreaPadding.top - + safeAreaPadding.bottom; + + print(availableHeight); return BlocBuilder( builder: (context, connectedState) { return Scaffold( @@ -61,7 +67,33 @@ class _ScannerPageState extends State // ), body: BlocBuilder>( builder: (context, state) { - return NestedScrollView( + return CustomScrollView( + physics: + state.isEmpty ? const NeverScrollableScrollPhysics() : null, + slivers: [ + SearchAppBar( + hintText: S.of(context).documentSearchSearchDocuments, + onOpenSearch: showDocumentSearchPage, + bottom: PreferredSize( + child: _buildActions(connectedState.isConnected), + preferredSize: const Size.fromHeight(kTextTabBarHeight), + ), + ), + if (state.isEmpty) + SliverToBoxAdapter( + child: SizedBox( + height: availableHeight, + child: Center( + child: _buildEmptyState(connectedState.isConnected), + ), + ), + ) + else + _buildImageGrid(state) + ], + ); + + NestedScrollView( floatHeaderSlivers: false, headerSliverBuilder: (context, innerBoxIsScrolled) => [ SearchAppBar( @@ -76,8 +108,9 @@ class _ScannerPageState extends State body: CustomScrollView( slivers: [ if (state.isEmpty) - SliverFillRemaining( - child: _buildEmptyState(connectedState.isConnected), + SliverFillViewport( + delegate: SliverChildListDelegate.fixed( + [_buildEmptyState(connectedState.isConnected)]), ) else _buildImageGrid(state) @@ -229,13 +262,11 @@ class _ScannerPageState extends State child: BlocProvider( create: (context) => DocumentUploadCubit( documentApi: context.read(), - correspondentRepository: context.read< - LabelRepository>(), - documentTypeRepository: context.read< - LabelRepository>(), - tagRepository: - context.read>(), + correspondentRepository: + context.read>(), + documentTypeRepository: + context.read>(), + tagRepository: context.read>(), ), child: DocumentUploadPreparationPage( fileBytes: file.bytes, @@ -346,14 +377,11 @@ class _ScannerPageState extends State child: BlocProvider( create: (context) => DocumentUploadCubit( documentApi: context.read(), - correspondentRepository: context.read< - LabelRepository>(), - documentTypeRepository: context.read< - LabelRepository>(), - tagRepository: - context.read>(), + correspondentRepository: + context.read>(), + documentTypeRepository: + context.read>(), + tagRepository: context.read>(), ), child: DocumentUploadPreparationPage( fileBytes: file.readAsBytesSync(), diff --git a/lib/features/settings/view/dialogs/account_settings_dialog.dart b/lib/features/settings/view/dialogs/account_settings_dialog.dart index d3ed33c..9d2aba3 100644 --- a/lib/features/settings/view/dialogs/account_settings_dialog.dart +++ b/lib/features/settings/view/dialogs/account_settings_dialog.dart @@ -6,10 +6,6 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; @@ -26,9 +22,10 @@ class AccountSettingsDialog extends StatelessWidget { scrollable: true, contentPadding: EdgeInsets.zero, title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const CloseButton(), Text(S.of(context).accountSettingsTitle), + const CloseButton(), ], ), content: BlocBuilder().logout(); await context.read().clear(); - await context.read>().clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); - await context - .read>() - .clear(); + await context.read>().clear(); + await context.read>().clear(); + await context.read>().clear(); + await context.read>().clear(); await context.read().clear(); await HydratedBloc.storage.clear(); } on PaperlessServerException catch (error, stackTrace) { diff --git a/lib/features/similar_documents/cubit/similar_documents_cubit.dart b/lib/features/similar_documents/cubit/similar_documents_cubit.dart index dbaee29..1edb7fd 100644 --- a/lib/features/similar_documents/cubit/similar_documents_cubit.dart +++ b/lib/features/similar_documents/cubit/similar_documents_cubit.dart @@ -1,21 +1,32 @@ import 'package:bloc/bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; part 'similar_documents_state.dart'; class SimilarDocumentsCubit extends Cubit - with PagedDocumentsMixin { + with PagedDocumentsMixin { final int documentId; @override final PaperlessDocumentsApi api; + @override + final DocumentChangedNotifier notifier; + SimilarDocumentsCubit( - this.api, { + this.api, + this.notifier, { required this.documentId, - }) : super(const SimilarDocumentsState()); + }) : super(const SimilarDocumentsState()) { + notifier.subscribe( + this, + onDeleted: remove, + onUpdated: replace, + ); + } Future initialize() async { if (!state.hasLoaded) { diff --git a/lib/features/document_details/view/pages/similar_documents_view.dart b/lib/features/similar_documents/view/similar_documents_view.dart similarity index 77% rename from lib/features/document_details/view/pages/similar_documents_view.dart rename to lib/features/similar_documents/view/similar_documents_view.dart index 01c5fa2..0092e44 100644 --- a/lib/features/document_details/view/pages/similar_documents_view.dart +++ b/lib/features/similar_documents/view/similar_documents_view.dart @@ -2,14 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/constants.dart'; +import 'package:paperless_mobile/routes/document_details_route.dart'; class SimilarDocumentsView extends StatefulWidget { const SimilarDocumentsView({super.key}); @@ -54,13 +53,9 @@ class _SimilarDocumentsViewState extends State { @override Widget build(BuildContext context) { - const earlyPreviewHintCard = HintCard( - hintIcon: Icons.construction, - hintText: "This view is still work in progress.", - ); return BlocBuilder( builder: (context, state) { - if (state.documents.isEmpty) { + if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { return DocumentsEmptyState( state: state, onReset: () => context.read().updateFilter( @@ -77,26 +72,23 @@ class _SimilarDocumentsViewState extends State { return CustomScrollView( controller: _scrollController, slivers: [ - const SliverToBoxAdapter(child: earlyPreviewHintCard), SliverAdaptiveDocumentsView( documents: state.documents, hasInternetConnection: connectivity.isConnected, isLabelClickable: false, isLoading: state.isLoading, hasLoaded: state.hasLoaded, - - ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: state.documents.length, - (context, index) => DocumentListItem( - document: state.documents[index], - enableHeroAnimation: false, - isLabelClickable: false, - isSelected: false, - isSelectionActive: false, - ), - ), + enableHeroAnimation: false, + onTap: (document) { + Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, + ), + ); + }, ), ], ); diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index ddfe2d9..58842a3 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Add another account", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Nový korespondent", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "Jste offline.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "Assign ASN", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Delete document", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "Assign ASN", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Dokument odstraněn z inboxu.", diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 6be28f4..1e29e7f 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Einen Account hinzufügen", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Neuer Korrespondent", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "Du bist offline.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "ASN zuweisen", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Dokument löschen", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "ASN zuweisen", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 8ab7807..9d5124c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Add another account", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "New Correspondent", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "You're offline.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "Assign ASN", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Delete document", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "Assign ASN", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Document removed from inbox.", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index d8e6a7f..6b6b654 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Add another account", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "New Correspondent", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "Jesteście w trybie offline.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "Assign ASN", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Delete document", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "Assign ASN", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Dokument usunięty ze skrzynki odbiorczej", diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 3451880..25b7b62 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -6,6 +6,8 @@ "name": {} } }, + "accountSettingsAddAnotherAccount": "Add another account", + "@accountSettingsAddAnotherAccount": {}, "accountSettingsTitle": "Account", "@accountSettingsTitle": {}, "addCorrespondentPageTitle": "Yeni ek yazar", @@ -396,6 +398,10 @@ "@genericActionUploadLabel": {}, "genericMessageOfflineText": "Çevrimdışısınız.", "@genericMessageOfflineText": {}, + "inboxActionAssignAsn": "Assign ASN", + "@inboxActionAssignAsn": {}, + "inboxActionDeleteDocument": "Delete document", + "@inboxActionDeleteDocument": {}, "inboxPageAssignAsnLabel": "ASN ata", "@inboxPageAssignAsnLabel": {}, "inboxPageDocumentRemovedMessageText": "Döküman gelen kutusundan kaldırıldı.", diff --git a/lib/main.dart b/lib/main.dart index 4239ff2..817dc89 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -109,7 +109,7 @@ void main() async { final connectivityCubit = ConnectivityCubit(connectivityStatusService); // Remove temporarily downloaded files. - (await FileService.temporaryDirectory).deleteSync(recursive: true); + // (await FileService.temporaryDirectory).deleteSync(recursive: true); // Load application settings and stored authentication data await connectivityCubit.initialize(); @@ -173,20 +173,16 @@ void main() async { ], child: MultiRepositoryProvider( providers: [ - RepositoryProvider>.value( + RepositoryProvider>.value( value: tagRepository, ), - RepositoryProvider< - LabelRepository>.value( + RepositoryProvider>.value( value: correspondentRepository, ), - RepositoryProvider< - LabelRepository>.value( + RepositoryProvider>.value( value: documentTypeRepository, ), - RepositoryProvider< - LabelRepository>.value( + RepositoryProvider>.value( value: storagePathRepository, ), RepositoryProvider.value( diff --git a/lib/routes/document_details_route.dart b/lib/routes/document_details_route.dart index 36a0fad..8db47c3 100644 --- a/lib/routes/document_details_route.dart +++ b/lib/routes/document_details_route.dart @@ -17,8 +17,9 @@ class DocumentDetailsRoute extends StatelessWidget { return BlocProvider( create: (context) => DocumentDetailsCubit( - context.read(), - args.document, + context.read(), + context.read(), + initialDocument: args.document, ), child: LabelRepositoriesProvider( child: DocumentDetailsPage( diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index d872076..8ac6d99 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -146,7 +146,20 @@ class DocumentFilter extends Equatable { /// /// Checks whether the properties of [document] match the current filter criteria. /// - bool includes(DocumentModel document) {} + bool matches(DocumentModel document) { + return correspondent.matches(document.correspondent) && + documentType.matches(document.documentType) && + storagePath.matches(document.storagePath) && + tags.matches(document.tags) && + created.matches(document.created) && + added.matches(document.added) && + modified.matches(document.modified) && + query.matches( + title: document.title, + content: document.content, + asn: document.archiveSerialNumber, + ); + } int get appliedFiltersCount => [ documentType != initial.documentType, diff --git a/packages/paperless_api/lib/src/models/paged_search_result.dart b/packages/paperless_api/lib/src/models/paged_search_result.dart index 9beef0c..e426ed1 100644 --- a/packages/paperless_api/lib/src/models/paged_search_result.dart +++ b/packages/paperless_api/lib/src/models/paged_search_result.dart @@ -1,6 +1,5 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/models/document_model.dart'; const pageRegex = r".*page=(\d+).*"; @@ -108,5 +107,10 @@ class PagedSearchResult extends Equatable { } @override - List get props => [count, next, previous, results]; + List get props => [ + count, + next, + previous, + results, + ]; } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart index 56d7ecc..73a3665 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart @@ -52,4 +52,17 @@ class AbsoluteDateRangeQuery extends DateRangeQuery { @override Map toJson() => _$AbsoluteDateRangeQueryToJson(this); + + @override + bool matches(DateTime dt) { + //TODO: Check if after and before are inclusive or exclusive definitions. + bool matches = true; + if (after != null) { + matches &= dt.isAfter(after!) || dt == after; + } + if (before != null) { + matches &= dt.isBefore(before!) || dt == before; + } + return matches; + } } 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 5ce1fe8..7a7c6f7 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 @@ -7,4 +7,6 @@ abstract class DateRangeQuery extends Equatable { Map toQueryParameter(DateRangeQueryField field); Map toJson(); + + bool matches(DateTime dt); } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart index ae435b1..ef5ea56 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart @@ -1,3 +1,4 @@ +import 'package:jiffy/jiffy.dart'; import 'package:json_annotation/json_annotation.dart'; import 'date_range_query.dart'; @@ -35,9 +36,28 @@ class RelativeDateRangeQuery extends DateRangeQuery { ); } + /// Returns the datetime when subtracting the offset given the unit from now. + DateTime get dateTime { + switch (unit) { + case DateRangeUnit.day: + return Jiffy().subtract(days: offset).dateTime; + case DateRangeUnit.week: + return Jiffy().subtract(weeks: offset).dateTime; + case DateRangeUnit.month: + return Jiffy().subtract(months: offset).dateTime; + case DateRangeUnit.year: + return Jiffy().subtract(years: offset).dateTime; + } + } + @override Map toJson() => _$RelativeDateRangeQueryToJson(this); factory RelativeDateRangeQuery.fromJson(Map json) => _$RelativeDateRangeQueryFromJson(json); + + @override + bool matches(DateTime dt) { + return dt.isAfter(dateTime) || dt == dateTime; + } } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart index 055130f..1a0e0aa 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart @@ -14,4 +14,7 @@ class UnsetDateRangeQuery extends DateRangeQuery { Map toJson() { return {}; } + + @override + bool matches(DateTime dt) => true; } 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 1890e2a..21e1884 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 @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; part 'id_query_parameter.g.dart'; @@ -19,9 +18,7 @@ class IdQueryParameter extends Equatable { : assignmentStatus = 0, id = null; - const IdQueryParameter.fromId(int? id) - : assignmentStatus = null, - id = id; + const IdQueryParameter.fromId(this.id) : assignmentStatus = null; const IdQueryParameter.unset() : this.fromId(null); @@ -45,6 +42,13 @@ class IdQueryParameter extends Equatable { return params; } + bool matches(int? id) { + return onlyAssigned && id != null || + onlyNotAssigned && id == null || + isSet && id == this.id || + isUnset; + } + @override List get props => [assignmentStatus, id]; diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart index adf5a25..36b1a03 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart @@ -27,4 +27,9 @@ class AnyAssignedTagsQuery extends TagsQuery { factory AnyAssignedTagsQuery.fromJson(Map json) => _$AnyAssignedTagsQueryFromJson(json); + + @override + bool matches(Iterable ids) { + return ids.isNotEmpty; + } } diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart index 834fc14..f8802f0 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; - +import 'package:collection/collection.dart'; import 'exclude_tag_id_query.dart'; import 'include_tag_id_query.dart'; import 'tag_id_query.dart'; @@ -85,4 +85,10 @@ class IdsTagsQuery extends TagsQuery { (json['queries'] as List).map((e) => TagIdQuery.fromJson(e)), ); } + + @override + bool matches(Iterable ids) { + return includedIds.toSet().difference(ids.toSet()).isEmpty && + excludedIds.toSet().intersection(ids.toSet()).isEmpty; + } } diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart index 0c0d937..6d24678 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart @@ -14,4 +14,9 @@ class OnlyNotAssignedTagsQuery extends TagsQuery { Map toJson() { return {}; } + + @override + bool matches(Iterable ids) { + return ids.isEmpty; + } } 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 b9de435..984d846 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,4 +4,6 @@ abstract class TagsQuery extends Equatable { const TagsQuery(); Map toQueryParameter(); Map toJson(); + + bool matches(Iterable ids); } 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 78b1123..fdebfb8 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 @@ -58,6 +58,26 @@ class TextQuery extends Equatable { return null; } + bool matches({ + required String title, + String? content, + int? asn, + }) { + if (queryText?.isEmpty ?? true) return true; + switch (queryType) { + case QueryType.title: + return title.contains(queryText!); + case QueryType.titleAndContent: + return title.contains(queryText!) || + (content?.contains(queryText!) ?? false); + case QueryType.extended: + //TODO: Implement. Might be too complex... + return true; + case QueryType.asn: + return int.tryParse(queryText!) == asn; + } + } + Map toJson() => _$TextQueryToJson(this); factory TextQuery.fromJson(Map json) => diff --git a/packages/paperless_api/pubspec.yaml b/packages/paperless_api/pubspec.yaml index 91162d0..396538a 100644 --- a/packages/paperless_api/pubspec.yaml +++ b/packages/paperless_api/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: intl: ^0.17.0 dio: ^4.0.6 collection: ^1.17.0 + jiffy: ^5.0.0 dev_dependencies: flutter_test: diff --git a/pubspec.lock b/pubspec.lock index 7be9f14..452df54 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8c7478991c7bbde2c1e18034ac697723176a5d3e7e0ca06c7f9aed69b6f388d7" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" url: "https://pub.dev" source: hosted - version: "51.0.0" + version: "52.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "120fe7ce25377ba616bb210e7584983b163861f45d6ec446744d507e3943881b" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "5.4.0" analyzer_plugin: dependency: transitive description: @@ -37,18 +37,18 @@ packages: dependency: transitive description: name: archive - sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb" + sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.3.6" args: dependency: transitive description: name: args - sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 + sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" asn1lib: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: a3335cae313ea41f193e5637f98185e5cb37b3fde2c5c4654ac546b8164e59ac url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" build_runner_core: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: built_value - sha256: "59e08b0079bb75f7e27392498e26339387c1089c6bd58525a14eb8508637277b" + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.3" cached_network_image: dependency: "direct main" description: @@ -245,10 +245,10 @@ packages: dependency: "direct main" description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" connectivity_plus: dependency: "direct main" description: @@ -309,18 +309,18 @@ packages: dependency: transitive description: name: coverage - sha256: d2494157c32b303f47dedee955b1479f2979c4ff66934eb7c0def44fd9e0267a + sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "1.6.2" cross_file: dependency: transitive description: name: cross_file - sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" url: "https://pub.dev" source: hosted - version: "0.3.3+2" + version: "0.3.3+4" crypto: dependency: transitive description: @@ -341,10 +341,10 @@ packages: dependency: "direct dev" description: name: dart_code_metrics - sha256: "95f22e95638c0dfb0cb4e3ba45e00bb06dd509c98f06d4c0fa45340b0a5392e0" + sha256: bb4ec5e729788dde5f7e8e9df4c05ec3b78532a5763e635337153ce40085514b url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.5.1" dart_code_metrics_presets: dependency: transitive description: @@ -453,16 +453,16 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: "37a15576f5a0bfd5555b613cf20ea3bd379607cf88d457374a16032f4e942174" + sha256: c4a508284b14ec4dda5adba2c28b2cdd34fbae1afead7e8c52cad87d51c5405b url: "https://pub.dev" source: hosted - version: "1.5.4" + version: "1.6.2" edge_detection: dependency: "direct main" description: path: "." ref: master - resolved-ref: "8c80e3a6e231985763ff501ad7ae12d76995a2e8" + resolved-ref: "24da81d7cb3bc6418d5901da355addb337793b46" url: "https://github.com/sawankumarbundelkhandi/edge_detection" source: git version: "1.1.1" @@ -526,18 +526,18 @@ packages: dependency: "direct main" description: name: file_picker - sha256: ecf52f978e72763ede54a93271318bbbca65a2be2d9ff658ec8ca4ea3a23d7ef + sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 url: "https://pub.dev" source: hosted - version: "5.2.4" + version: "5.2.5" fixnum: dependency: transitive description: name: fixnum - sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -865,42 +865,50 @@ packages: dependency: "direct main" description: name: introduction_screen - sha256: "26d06cff940b9f3f1ec6591a6beea4da31183574b279c373e142ca76882ce9ea" + sha256: "73965475d6b271846f81c5fce5b459546a4ea36c285408691522437fd6bbeb69" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" + jiffy: + dependency: transitive + description: + name: jiffy + sha256: "85172c4fc975a50224521c05bf43abc845288863b19d91bd3c221a96a8785dd3" + url: "https://pub.dev" + source: hosted + version: "5.0.0" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.8.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a url: "https://pub.dev" source: hosted - version: "6.5.4" + version: "6.6.1" lints: dependency: transitive description: @@ -945,18 +953,18 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: "53ef7487587e1cb06755861a9a74585b3b361ba1969ad374c728c75771a14fbb" + sha256: "888482e4f9ca3560e00bc227ce2badeb4857aad450c42a31c6cfc9dc21e0ccbc" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" logging: dependency: transitive description: name: logging - sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946 + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: @@ -977,18 +985,18 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "12307e7f0605ce3da64cf0db90e5fcab0869f3ca03f76be6bb2991ce0a55e82b" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.0" mime: dependency: "direct main" description: name: mime - sha256: "52e38f7e1143ef39daf532117d6b8f8f617bf4bcd6044ed8c29040d20d269630" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" mockito: dependency: "direct dev" description: @@ -1136,10 +1144,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "050e8e85e4b7fecdf2bb3682c1c64c4887a183720c802d323de8a5fd76d372dd" + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.12" path_provider_android: dependency: transitive description: @@ -1148,14 +1156,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.22" - path_provider_ios: + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8" + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.1" path_provider_linux: dependency: transitive description: @@ -1164,14 +1172,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - sha256: "2a97e7fbb7ae9dcd0dfc1220a78e9ec3e71da691912e617e8715ff2a13086ae8" - url: "https://pub.dev" - source: hosted - version: "2.0.6" path_provider_platform_interface: dependency: transitive description: @@ -1336,10 +1336,10 @@ packages: dependency: transitive description: name: pub_updater - sha256: "00e42b515aa046b171d05bbe2dd566c0feaab7808c33c5bacb5beff93cf16561" + sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4" pubspec_parse: dependency: transitive description: @@ -1400,42 +1400,34 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "76917b7d4b9526b2ba416808a7eb9fb2863c1a09cf63ec85f1453da240fa818a" + sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.17" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8e251f3c986002b65fed6396bce81f379fb63c27317d49743cf289fd0fd1ab97" + sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" url: "https://pub.dev" source: hosted - version: "2.0.14" - shared_preferences_ios: + version: "2.0.15" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios - sha256: "585a14cefec7da8c9c2fb8cd283a3bb726b4155c0952afe6a0caaa7b2272de34" + name: shared_preferences_foundation + sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: fbc3cd6826896b66a5f576b025e4f344f780c84ea7f8203097a353370607a2c8 + sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 url: "https://pub.dev" source: hosted - version: "2.1.2" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - sha256: fbb94bf296576f49be37a1496d5951796211a8db0aa22cc0d68c46440dad808c - url: "https://pub.dev" - source: hosted - version: "2.0.4" + version: "2.1.3" shared_preferences_platform_interface: dependency: transitive description: @@ -1456,10 +1448,10 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: "07c274c2115d4d5e4280622abb09f0980e2c5b1fcdc98ae9f59a3bad5bfc1f26" + sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" shelf: dependency: transitive description: @@ -1509,10 +1501,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.2.7" source_helper: dependency: transitive description: @@ -1549,18 +1541,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "2b1697c7b78576fdc722c358f16f62171bd56e92dc13422d9e44be3fc446c276" + sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "0c21a187d645aa65da5be6997c0c713eed61e049158870ae2de157e6897067ab" + sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f url: "https://pub.dev" source: hosted - version: "2.4.0+2" + version: "2.4.2+2" stack_trace: dependency: transitive description: @@ -1605,10 +1597,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "7b530acd9cb7c71b0019a1e7fa22c4105e675557a4400b6a401c71c5e0ade1ac" + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" url: "https://pub.dev" source: hosted - version: "3.0.0+3" + version: "3.0.1" term_glyph: dependency: transitive description: @@ -1621,26 +1613,26 @@ packages: dependency: transitive description: name: test - sha256: "98403d1090ac0aa9e33dfc8bf45cc2e0c1d5c58d7cb832cee1e50bf14f37961d" + sha256: b54d427664c00f2013ffb87797a698883c46aee9288e027a50b46eaee7486fa2 url: "https://pub.dev" source: hosted - version: "1.22.1" + version: "1.22.2" test_api: dependency: transitive description: name: test_api - sha256: c9282698e2982b6c3817037554e52f99d4daba493e8028f8112a83d68ccd0b12 + sha256: "6182294da5abf431177fccc1ee02401f6df30f766bc6130a0852c6b6d7ee6b2d" url: "https://pub.dev" source: hosted - version: "0.4.17" + version: "0.4.18" test_core: dependency: transitive description: name: test_core - sha256: c9e4661a5e6285b795d47ba27957ed8b6f980fc020e98b218e276e88aff02168 + sha256: "95ecc12692d0dd59080ab2d38d9cf32c7e9844caba23ff6cd285690398ee8ef4" url: "https://pub.dev" source: hosted - version: "0.4.21" + version: "0.4.22" timezone: dependency: transitive description: @@ -1653,10 +1645,10 @@ packages: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: @@ -1685,42 +1677,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "3c92b0efb5e9dcb8f846aefabf9f0f739f91682ed486b991ceda51c288e60896" + sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809" url: "https://pub.dev" source: hosted - version: "6.1.7" + version: "6.1.8" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "6f91d30ce9060c204b2dbe728adb300750fa4b228e8f7ed1b961aa1ceb728799" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" url: "https://pub.dev" source: hosted - version: "6.0.22" + version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "6ba7dddee26c9fae27c9203c424631109d73c8fa26cfa7bc3e35e751cb87f62e" + sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.0.18" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "360fa359ab06bcb4f7c5cd3123a2a9a4d3364d4575d27c4b33468bd4497dd094" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: a9b3ea9043eabfaadfa3fb89de67a11210d85569086d22b3854484beab8b3978 + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: @@ -1733,18 +1725,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "5669882643b96bb6d5786637cac727c6e918a790053b09245fd4513b8a07df2a" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: e3c3b16d3104260c10eea3b0e34272aaa57921f83148b0619f74c2eced9b7ef1 + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" uuid: dependency: "direct main" description: @@ -1765,10 +1757,10 @@ packages: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + sha256: "2277c73618916ae3c2082b6df67b6ebb64b4c69d9bf23b23700707952ac30e60" url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "10.1.2" watcher: dependency: transitive description: @@ -1781,18 +1773,18 @@ packages: dependency: "direct main" description: name: web_socket_channel - sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" webdriver: dependency: transitive description: name: webdriver - sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: @@ -1813,10 +1805,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: "11541eedefbcaec9de35aa82650b695297ce668662bbd6e3911a7fabdbde589f" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 url: "https://pub.dev" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" xml: dependency: transitive description: @@ -1834,5 +1826,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.0.0-35.0.dev <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.0.0-134.0.dev <4.0.0" + flutter: ">=3.4.0-17.0.pre" diff --git a/pubspec.yaml b/pubspec.yaml index eea5a10..99aa8e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -150,7 +150,6 @@ flutter: flutter_intl: enabled: true main_locale: en - localizely: project_id: 84b4144d-a628-4ba6-a8d0-4f9917444057 download_empty_as: main From 313ed1fad7183688f0e30c69f3decaa957597e41 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 01:05:14 +0100 Subject: [PATCH 19/25] Bump version number to 2.0.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 99aa8e8..d7b8210 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,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: 1.5.3+16 +version: 2.0.0+17 environment: sdk: '>=3.0.0-35.0.dev <4.0.0' From c9d86c6120bc1d23c6791c564a35ed0775f3bb49 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 12:38:00 +0100 Subject: [PATCH 20/25] Add newly generated files --- pubspec.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 452df54..db2f9c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb" url: "https://pub.dev" source: hosted - version: "3.3.6" + version: "3.3.2" args: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: a3335cae313ea41f193e5637f98185e5cb37b3fde2c5c4654ac546b8164e59ac + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.3.3" build_runner_core: dependency: transitive description: @@ -1826,5 +1826,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.0.0-134.0.dev <4.0.0" + dart: ">=3.0.0-35.0.dev <4.0.0" flutter: ">=3.4.0-17.0.pre" From 69b413d789056c35804eea2ee93154b5df80db29 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 12:40:25 +0100 Subject: [PATCH 21/25] Change default theme to classic --- lib/features/settings/bloc/application_settings_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/settings/bloc/application_settings_state.dart b/lib/features/settings/bloc/application_settings_state.dart index 5771bdd..108a13a 100644 --- a/lib/features/settings/bloc/application_settings_state.dart +++ b/lib/features/settings/bloc/application_settings_state.dart @@ -28,7 +28,7 @@ class ApplicationSettingsState { this.preferredThemeMode = ThemeMode.system, this.isLocalAuthenticationEnabled = false, this.preferredViewType = ViewType.list, - this.preferredColorSchemeOption = ColorSchemeOption.dynamic, + this.preferredColorSchemeOption = ColorSchemeOption.classic, }); Map toJson() => _$ApplicationSettingsStateToJson(this); From 7976beab16bd2a9e39b1f91474fd8dfbc8ef003b Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 13:00:02 +0100 Subject: [PATCH 22/25] Update generated settings, added correct signing config --- android/app/build.gradle | 2 +- lib/features/settings/bloc/application_settings_state.g.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 43710e2..20e446a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -70,7 +70,7 @@ android { } buildTypes { release { - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } } diff --git a/lib/features/settings/bloc/application_settings_state.g.dart b/lib/features/settings/bloc/application_settings_state.g.dart index a131830..c06b98b 100644 --- a/lib/features/settings/bloc/application_settings_state.g.dart +++ b/lib/features/settings/bloc/application_settings_state.g.dart @@ -20,7 +20,7 @@ ApplicationSettingsState _$ApplicationSettingsStateFromJson( ViewType.list, preferredColorSchemeOption: $enumDecodeNullable( _$ColorSchemeOptionEnumMap, json['preferredColorSchemeOption']) ?? - ColorSchemeOption.dynamic, + ColorSchemeOption.classic, ); Map _$ApplicationSettingsStateToJson( From 348eb30e1a89cf7a385c14d52bb8208fae28b01d Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 17:30:00 +0100 Subject: [PATCH 23/25] Fixes labels being serialized incorrectly, automates tag serialization --- lib/features/edit_label/view/label_form.dart | 17 +- .../converters/hex_color_json_converter.dart | 31 ++++ .../models/labels/correspondent_model.dart | 5 +- .../models/labels/correspondent_model.g.dart | 7 +- .../models/labels/document_type_model.dart | 4 +- .../models/labels/document_type_model.g.dart | 7 +- .../lib/src/models/labels/label_model.dart | 15 +- .../src/models/labels/matching_algorithm.dart | 9 +- .../src/models/labels/storage_path_model.dart | 10 +- .../models/labels/storage_path_model.g.dart | 11 +- .../lib/src/models/labels/tag_model.dart | 157 +++++++----------- .../lib/src/models/labels/tag_model.g.dart | 47 ++++++ 12 files changed, 175 insertions(+), 145 deletions(-) create mode 100644 packages/paperless_api/lib/src/converters/hex_color_json_converter.dart create mode 100644 packages/paperless_api/lib/src/models/labels/tag_model.g.dart diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index 8fa7327..1c0d1ea 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -54,8 +54,9 @@ class _LabelFormState extends State> { @override void initState() { super.initState(); - _enableMatchFormField = - widget.initialValue?.matchingAlgorithm != MatchingAlgorithm.auto; + _enableMatchFormField = (widget.initialValue?.matchingAlgorithm ?? + MatchingAlgorithm.defaultValue) != + MatchingAlgorithm.auto; } @override @@ -83,8 +84,9 @@ class _LabelFormState extends State> { ), FormBuilderDropdown( name: Label.matchingAlgorithmKey, - initialValue: widget.initialValue?.matchingAlgorithm.value ?? - MatchingAlgorithm.auto.value, + initialValue: (widget.initialValue?.matchingAlgorithm ?? + MatchingAlgorithm.defaultValue) + .value, decoration: InputDecoration( labelText: S.of(context).labelMatchingAlgorithmPropertyLabel, errorText: _errors[Label.matchingAlgorithmKey], @@ -99,7 +101,8 @@ class _LabelFormState extends State> { .map( (algo) => DropdownMenuItem( child: Text( - translateMatchingAlgorithmDescription(context, algo)), + translateMatchingAlgorithmDescription(context, algo), + ), value: algo.value, ), ) @@ -139,8 +142,8 @@ class _LabelFormState extends State> { // If auto is selected, the match will be removed. mergedJson[Label.matchKey] = ''; } - final createdLabel = await widget.submitButtonConfig - .onSubmit(widget.fromJsonT(mergedJson)); + final parsed = widget.fromJsonT(mergedJson); + final createdLabel = await widget.submitButtonConfig.onSubmit(parsed); Navigator.pop(context, createdLabel); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); diff --git a/packages/paperless_api/lib/src/converters/hex_color_json_converter.dart b/packages/paperless_api/lib/src/converters/hex_color_json_converter.dart new file mode 100644 index 0000000..3a73baf --- /dev/null +++ b/packages/paperless_api/lib/src/converters/hex_color_json_converter.dart @@ -0,0 +1,31 @@ +import 'dart:ui'; + +import 'package:json_annotation/json_annotation.dart'; + +class HexColorJsonConverter implements JsonConverter { + const HexColorJsonConverter(); + @override + Color? fromJson(dynamic json) { + if (json is Color) { + return json; + } + if (json is String) { + final decoded = int.tryParse(json.replaceAll("#", "ff"), radix: 16); + if (decoded == null) { + return null; + } + return Color(decoded); + } + return null; + } + + @override + String? toJson(Color? color) { + if (color == null) { + return null; + } + String val = + '#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}'; + return val; + } +} diff --git a/packages/paperless_api/lib/src/models/labels/correspondent_model.dart b/packages/paperless_api/lib/src/models/labels/correspondent_model.dart index e0602a1..2a7523b 100644 --- a/packages/paperless_api/lib/src/models/labels/correspondent_model.dart +++ b/packages/paperless_api/lib/src/models/labels/correspondent_model.dart @@ -11,11 +11,11 @@ class Correspondent extends Label { final DateTime? lastCorrespondence; const Correspondent({ - required super.id, + super.id, required super.name, super.slug, super.match, - required super.matchingAlgorithm, + super.matchingAlgorithm, super.isInsensitive, super.documentCount, this.lastCorrespondence, @@ -24,6 +24,7 @@ class Correspondent extends Label { factory Correspondent.fromJson(Map json) => _$CorrespondentFromJson(json); + @override Map toJson() => _$CorrespondentToJson(this); @override diff --git a/packages/paperless_api/lib/src/models/labels/correspondent_model.g.dart b/packages/paperless_api/lib/src/models/labels/correspondent_model.g.dart index 7354ce7..abd32f6 100644 --- a/packages/paperless_api/lib/src/models/labels/correspondent_model.g.dart +++ b/packages/paperless_api/lib/src/models/labels/correspondent_model.g.dart @@ -12,9 +12,10 @@ Correspondent _$CorrespondentFromJson(Map json) => name: json['name'] as String, slug: json['slug'] as String?, match: json['match'] as String?, - matchingAlgorithm: - $enumDecode(_$MatchingAlgorithmEnumMap, json['matching_algorithm']), - isInsensitive: json['is_insensitive'] as bool?, + matchingAlgorithm: $enumDecodeNullable( + _$MatchingAlgorithmEnumMap, json['matching_algorithm']) ?? + MatchingAlgorithm.defaultValue, + isInsensitive: json['is_insensitive'] as bool? ?? true, documentCount: json['document_count'] as int?, lastCorrespondence: _$JsonConverterFromJson( json['last_correspondence'], diff --git a/packages/paperless_api/lib/src/models/labels/document_type_model.dart b/packages/paperless_api/lib/src/models/labels/document_type_model.dart index 76be83e..085f822 100644 --- a/packages/paperless_api/lib/src/models/labels/document_type_model.dart +++ b/packages/paperless_api/lib/src/models/labels/document_type_model.dart @@ -6,11 +6,11 @@ part 'document_type_model.g.dart'; @JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) class DocumentType extends Label { const DocumentType({ - required super.id, + super.id, required super.name, super.slug, super.match, - required super.matchingAlgorithm, + super.matchingAlgorithm, super.isInsensitive, super.documentCount, }); diff --git a/packages/paperless_api/lib/src/models/labels/document_type_model.g.dart b/packages/paperless_api/lib/src/models/labels/document_type_model.g.dart index 93567b1..be8b0eb 100644 --- a/packages/paperless_api/lib/src/models/labels/document_type_model.g.dart +++ b/packages/paperless_api/lib/src/models/labels/document_type_model.g.dart @@ -11,9 +11,10 @@ DocumentType _$DocumentTypeFromJson(Map json) => DocumentType( name: json['name'] as String, slug: json['slug'] as String?, match: json['match'] as String?, - matchingAlgorithm: - $enumDecode(_$MatchingAlgorithmEnumMap, json['matching_algorithm']), - isInsensitive: json['is_insensitive'] as bool?, + matchingAlgorithm: $enumDecodeNullable( + _$MatchingAlgorithmEnumMap, json['matching_algorithm']) ?? + MatchingAlgorithm.defaultValue, + isInsensitive: json['is_insensitive'] as bool? ?? true, documentCount: json['document_count'] as int?, ); diff --git a/packages/paperless_api/lib/src/models/labels/label_model.dart b/packages/paperless_api/lib/src/models/labels/label_model.dart index 99111ed..1b78ccd 100644 --- a/packages/paperless_api/lib/src/models/labels/label_model.dart +++ b/packages/paperless_api/lib/src/models/labels/label_model.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; abstract class Label extends Equatable implements Comparable { @@ -12,27 +11,21 @@ abstract class Label extends Equatable implements Comparable { static const documentCountKey = "document_count"; String get queryEndpoint; - @JsonKey() + final int? id; - @JsonKey() final String name; - @JsonKey() final String? slug; - @JsonKey() final String? match; - @JsonKey() final MatchingAlgorithm matchingAlgorithm; - @JsonKey() final bool? isInsensitive; - @JsonKey() final int? documentCount; const Label({ - required this.id, + this.id, required this.name, - required this.matchingAlgorithm, + this.matchingAlgorithm = MatchingAlgorithm.defaultValue, this.match, - this.isInsensitive, + this.isInsensitive = true, this.documentCount, this.slug, }); diff --git a/packages/paperless_api/lib/src/models/labels/matching_algorithm.dart b/packages/paperless_api/lib/src/models/labels/matching_algorithm.dart index 1d6fd4b..b4229f9 100644 --- a/packages/paperless_api/lib/src/models/labels/matching_algorithm.dart +++ b/packages/paperless_api/lib/src/models/labels/matching_algorithm.dart @@ -14,12 +14,5 @@ enum MatchingAlgorithm { const MatchingAlgorithm(this.value, this.name); - static MatchingAlgorithm fromInt(int? value) { - return MatchingAlgorithm.values - .where((element) => element.value == value) - .firstWhere( - (element) => true, - orElse: () => MatchingAlgorithm.anyWord, - ); - } + static const MatchingAlgorithm defaultValue = auto; } diff --git a/packages/paperless_api/lib/src/models/labels/storage_path_model.dart b/packages/paperless_api/lib/src/models/labels/storage_path_model.dart index 0ac9c33..09566cc 100644 --- a/packages/paperless_api/lib/src/models/labels/storage_path_model.dart +++ b/packages/paperless_api/lib/src/models/labels/storage_path_model.dart @@ -6,17 +6,17 @@ part 'storage_path_model.g.dart'; @JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) class StoragePath extends Label { static const pathKey = 'path'; - late String? path; + final String path; - StoragePath({ - required super.id, + const StoragePath({ + super.id, required super.name, + required this.path, super.slug, super.match, - required super.matchingAlgorithm, + super.matchingAlgorithm, super.isInsensitive, super.documentCount, - required this.path, }); factory StoragePath.fromJson(Map json) => diff --git a/packages/paperless_api/lib/src/models/labels/storage_path_model.g.dart b/packages/paperless_api/lib/src/models/labels/storage_path_model.g.dart index 8c1211e..5ae4ad4 100644 --- a/packages/paperless_api/lib/src/models/labels/storage_path_model.g.dart +++ b/packages/paperless_api/lib/src/models/labels/storage_path_model.g.dart @@ -9,13 +9,14 @@ part of 'storage_path_model.dart'; StoragePath _$StoragePathFromJson(Map json) => StoragePath( id: json['id'] as int?, name: json['name'] as String, + path: json['path'] as String, slug: json['slug'] as String?, match: json['match'] as String?, - matchingAlgorithm: - $enumDecode(_$MatchingAlgorithmEnumMap, json['matching_algorithm']), - isInsensitive: json['is_insensitive'] as bool?, + matchingAlgorithm: $enumDecodeNullable( + _$MatchingAlgorithmEnumMap, json['matching_algorithm']) ?? + MatchingAlgorithm.defaultValue, + isInsensitive: json['is_insensitive'] as bool? ?? true, documentCount: json['document_count'] as int?, - path: json['path'] as String?, ); Map _$StoragePathToJson(StoragePath instance) { @@ -35,7 +36,7 @@ Map _$StoragePathToJson(StoragePath instance) { _$MatchingAlgorithmEnumMap[instance.matchingAlgorithm]!; writeNotNull('is_insensitive', instance.isInsensitive); writeNotNull('document_count', instance.documentCount); - writeNotNull('path', instance.path); + val['path'] = instance.path; return val; } diff --git a/packages/paperless_api/lib/src/models/labels/tag_model.dart b/packages/paperless_api/lib/src/models/labels/tag_model.dart index 62c4523..8f22e05 100644 --- a/packages/paperless_api/lib/src/models/labels/tag_model.dart +++ b/packages/paperless_api/lib/src/models/labels/tag_model.dart @@ -1,44 +1,84 @@ import 'dart:developer'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/src/converters/hex_color_json_converter.dart'; import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; +part 'tag_model.g.dart'; + +@HexColorJsonConverter() +@JsonSerializable( + fieldRename: FieldRename.snake, explicitToJson: true, constructor: "_") class Tag extends Label { static const colorKey = 'color'; static const isInboxTagKey = 'is_inbox_tag'; static const textColorKey = 'text_color'; static const legacyColourKey = 'colour'; - final Color? _apiV2color; - - final Color? _apiV1color; - final Color? textColor; final bool? isInboxTag; - Color? get color => _apiV2color ?? _apiV1color; + @protected + @JsonKey(name: colorKey) + Color? colorv2; - const Tag({ - required super.id, + @protected + @Deprecated( + "Alias for the field color. Deprecated since Paperless API v2. Please use the color getter to access the background color of this tag.", + ) + @JsonKey(name: legacyColourKey) + Color? colorv1; + + Color? get color => colorv2 ?? colorv1; + + /// Constructor to use for serialization. + Tag._({ + super.id, required super.name, super.documentCount, - super.isInsensitive, + super.isInsensitive = true, super.match, - required super.matchingAlgorithm, + super.matchingAlgorithm, super.slug, - Color? color, + this.colorv1, + this.colorv2, this.textColor, - this.isInboxTag, - }) : _apiV1color = color, - _apiV2color = color; + this.isInboxTag = false, + }) { + colorv1 ??= colorv2; + colorv2 ??= colorv1; + } + + Tag({ + int? id, + required String name, + int? documentCount, + bool? isInsensitive, + String? match, + MatchingAlgorithm matchingAlgorithm = MatchingAlgorithm.defaultValue, + String? slug, + Color? color, + Color? textColor, + bool? isInboxTag, + }) : this._( + id: id, + name: name, + documentCount: documentCount, + isInsensitive: isInsensitive, + match: match, + matchingAlgorithm: matchingAlgorithm, + slug: slug, + textColor: textColor, + colorv1: color, + colorv2: color, + ); @override - String toString() { - return name; - } + String toString() => name; @override Tag copyWith({ @@ -84,89 +124,8 @@ class Tag extends Label { match, ]; - //FIXME: Why is this not generated?! - factory Tag.fromJson(Map json) { - const $MatchingAlgorithmEnumMap = { - MatchingAlgorithm.anyWord: 1, - MatchingAlgorithm.allWords: 2, - MatchingAlgorithm.exactMatch: 3, - MatchingAlgorithm.regex: 4, - MatchingAlgorithm.fuzzy: 5, - MatchingAlgorithm.auto: 6, - }; - - return Tag( - id: json['id'] as int?, - name: json['name'] as String, - documentCount: json['document_count'] as int?, - isInsensitive: json['is_insensitive'] as bool?, - match: json['match'] as String?, - matchingAlgorithm: - $enumDecode($MatchingAlgorithmEnumMap, json['matching_algorithm']), - slug: json['slug'] as String?, - textColor: _colorFromJson(json['text_color']), - isInboxTag: json['is_inbox_tag'] as bool?, - color: _colorFromJson(json['color']) ?? _colorFromJson(json['colour']), - ); - } + factory Tag.fromJson(Map json) => _$TagFromJson(json); @override - Map toJson() { - final val = {}; - - const $MatchingAlgorithmEnumMap = { - MatchingAlgorithm.anyWord: 1, - MatchingAlgorithm.allWords: 2, - MatchingAlgorithm.exactMatch: 3, - MatchingAlgorithm.regex: 4, - MatchingAlgorithm.fuzzy: 5, - MatchingAlgorithm.auto: 6, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('id', id); - val['name'] = name; - writeNotNull('slug', slug); - writeNotNull('match', match); - writeNotNull( - 'matching_algorithm', $MatchingAlgorithmEnumMap[matchingAlgorithm]); - writeNotNull('is_insensitive', isInsensitive); - writeNotNull('document_count', documentCount); - writeNotNull('color', _toHex(_apiV2color)); - writeNotNull('colour', _toHex(_apiV1color)); - writeNotNull('text_color', _toHex(textColor)); - writeNotNull('is_inbox_tag', isInboxTag); - return val; - } - - static Color? _colorFromJson(dynamic color) { - if (color is Color) { - return color; - } - if (color is String) { - final decoded = int.tryParse(color.replaceAll("#", "ff"), radix: 16); - if (decoded == null) { - return null; - } - return Color(decoded); - } - return null; - } - - /// - /// Taken from [FormBuilderColorPicker]. - /// - static String? _toHex(Color? color) { - if (color == null) { - return null; - } - String val = - '#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}'; - return val; - } + Map toJson() => _$TagToJson(this); } diff --git a/packages/paperless_api/lib/src/models/labels/tag_model.g.dart b/packages/paperless_api/lib/src/models/labels/tag_model.g.dart new file mode 100644 index 0000000..9be6dc3 --- /dev/null +++ b/packages/paperless_api/lib/src/models/labels/tag_model.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tag_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Tag _$TagFromJson(Map json) => Tag._( + id: json['id'] as int?, + name: json['name'] as String, + documentCount: json['document_count'] as int?, + isInsensitive: json['is_insensitive'] as bool? ?? true, + match: json['match'] as String?, + matchingAlgorithm: $enumDecodeNullable( + _$MatchingAlgorithmEnumMap, json['matching_algorithm']) ?? + MatchingAlgorithm.defaultValue, + slug: json['slug'] as String?, + colorv1: const HexColorJsonConverter().fromJson(json['colour']), + colorv2: const HexColorJsonConverter().fromJson(json['color']), + textColor: const HexColorJsonConverter().fromJson(json['text_color']), + isInboxTag: json['is_inbox_tag'] as bool? ?? false, + ); + +Map _$TagToJson(Tag instance) => { + 'id': instance.id, + 'name': instance.name, + 'slug': instance.slug, + 'match': instance.match, + 'matching_algorithm': + _$MatchingAlgorithmEnumMap[instance.matchingAlgorithm]!, + 'is_insensitive': instance.isInsensitive, + 'document_count': instance.documentCount, + 'text_color': const HexColorJsonConverter().toJson(instance.textColor), + 'is_inbox_tag': instance.isInboxTag, + 'color': const HexColorJsonConverter().toJson(instance.colorv2), + 'colour': const HexColorJsonConverter().toJson(instance.colorv1), + }; + +const _$MatchingAlgorithmEnumMap = { + MatchingAlgorithm.anyWord: 1, + MatchingAlgorithm.allWords: 2, + MatchingAlgorithm.exactMatch: 3, + MatchingAlgorithm.regex: 4, + MatchingAlgorithm.fuzzy: 5, + MatchingAlgorithm.auto: 6, +}; From 7718c115a21890c4440a607a2f73db4740aed9cd Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 17:34:05 +0100 Subject: [PATCH 24/25] Removes dependency to auto_route --- pubspec.lock | 2 +- pubspec.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index db2f9c4..725549c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -66,7 +66,7 @@ packages: source: hosted version: "2.10.0" auto_route: - dependency: "direct main" + dependency: transitive description: name: auto_route sha256: "12047baeca0e01df93165ef33275b32119d72699ab9a49dc64c20e78f586f96d" diff --git a/pubspec.yaml b/pubspec.yaml index d7b8210..f0afd41 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,7 +88,6 @@ dependencies: responsive_builder: ^0.4.3 open_filex: ^4.3.2 dynamic_color: ^1.5.4 - auto_route: ^5.0.4 dev_dependencies: integration_test: From 41c6006f60b15eb3bed30526d01a50fbbe908182 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 6 Feb 2023 17:39:11 +0100 Subject: [PATCH 25/25] Cleanup main --- lib/main.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 10cf8f5..51dc5ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,7 @@ -import 'dart:developer'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm; @@ -30,14 +28,9 @@ import 'package:paperless_mobile/core/repository/impl/storage_path_repository_im import 'package:paperless_mobile/core/repository/impl/tag_repository_impl.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart'; import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/dio_file_service.dart'; -import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; @@ -146,11 +139,11 @@ void main() async { //Update language header in interceptor on language change. appSettingsCubit.stream.listen((event) => languageHeaderInterceptor .preferredLocaleSubtag = event.preferredLocaleSubtag); - + // Temporary Fix: Can be removed if the flutter engine implements the fix itself // Activate the highest availabe refresh rate on the device await FlutterDisplayMode.setHighRefreshRate(); - + runApp( MultiProvider( providers: [