From 00b8f317c587a0eba5b6f06922e071ba56161152 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 21 Jan 2023 14:47:18 +0100 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 e9e9fdc336fa35ea2884ffe3a46aad516d078af2 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 31 Jan 2023 00:29:07 +0100 Subject: [PATCH 11/20] 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 12/20] 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 13/20] 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 3f305ce1d6c1211ee03b3e6e0402d12cd099518b Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Fri, 3 Feb 2023 00:27:14 +0100 Subject: [PATCH 14/20] 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 15/20] 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 16/20] 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 17/20] 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 18/20] 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 19/20] 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 20/20] 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(