diff --git a/lib/features/document_search/cubit/document_search_state.dart b/lib/features/document_search/cubit/document_search_state.dart index 8855670..8a66296 100644 --- a/lib/features/document_search/cubit/document_search_state.dart +++ b/lib/features/document_search/cubit/document_search_state.dart @@ -7,18 +7,12 @@ enum SearchView { @JsonSerializable(ignoreUnannotated: true) class DocumentSearchState extends DocumentPagingState { - @JsonKey() final List searchHistory; final SearchView view; final List suggestions; @JsonKey() final ViewType viewType; - final Map correspondents; - final Map documentTypes; - final Map tags; - final Map storagePaths; - const DocumentSearchState({ this.view = SearchView.suggestions, this.searchHistory = const [], @@ -28,10 +22,6 @@ class DocumentSearchState extends DocumentPagingState { super.hasLoaded, super.isLoading, super.value, - this.correspondents = const {}, - this.documentTypes = const {}, - this.tags = const {}, - this.storagePaths = const {}, }); @override @@ -41,10 +31,6 @@ class DocumentSearchState extends DocumentPagingState { suggestions, view, viewType, - correspondents, - documentTypes, - tags, - storagePaths, ]; @override @@ -85,10 +71,6 @@ class DocumentSearchState extends DocumentPagingState { view: view ?? this.view, suggestions: suggestions ?? this.suggestions, viewType: viewType ?? this.viewType, - correspondents: correspondents ?? this.correspondents, - documentTypes: documentTypes ?? this.documentTypes, - tags: tags ?? this.tags, - storagePaths: storagePaths ?? this.storagePaths, ); } diff --git a/lib/features/document_search/view/document_search_bar.dart b/lib/features/document_search/view/document_search_bar.dart new file mode 100644 index 0000000..2d4f118 --- /dev/null +++ b/lib/features/document_search/view/document_search_bar.dart @@ -0,0 +1,211 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/navigation/push_routes.dart'; +import 'package:paperless_mobile/core/repository/label_repository.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/view/remove_history_entry_dialog.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; +import 'package:paperless_mobile/features/home/view/model/api_version.dart'; +import 'package:paperless_mobile/features/settings/model/view_type.dart'; +import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +class DocumentSearchBar extends StatefulWidget { + const DocumentSearchBar({super.key}); + + @override + State createState() => _DocumentSearchBarState(); +} + +class _DocumentSearchBarState extends State { + Timer? _debounceTimer; + + final _controller = SearchController(); + + @override + void initState() { + super.initState(); + _controller.addListener(() { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(milliseconds: 500), () { + context.read().suggest(query); + }); + }); + } + + late final DocumentSearchCubit _searchCubit; + String get query => _controller.text; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _searchCubit = context.watch(); + } + + @override + Widget build(BuildContext context) { + return SearchAnchor.bar( + searchController: _controller, + barLeading: IconButton( + icon: const Icon(Icons.menu), + onPressed: Scaffold.of(context).openDrawer, + ), + barHintText: S.of(context)!.searchDocuments, + barTrailing: [ + IconButton( + icon: GlobalSettingsBuilder( + builder: (context, settings) { + return ValueListenableBuilder( + valueListenable: + Hive.box(HiveBoxes.localUserAccount).listenable(), + builder: (context, box, _) { + final account = box.get(settings.currentLoggedInUser!)!; + return UserAvatar( + userId: settings.currentLoggedInUser!, + account: account, + ); + }, + ); + }, + ), + onPressed: () { + final apiVersion = context.read(); + showDialog( + context: context, + builder: (context) => Provider.value( + value: apiVersion, + child: const ManageAccountsPage(), + ), + ); + }, + ), + ], + suggestionsBuilder: (context, controller) { + switch (_searchCubit.state.view) { + case SearchView.suggestions: + return _buildSuggestionItems(_searchCubit.state); + case SearchView.results: + // TODO: Handle this case. + break; + } + }, + ); + } + + Iterable _buildSuggestionItems(DocumentSearchState state) sync* { + final suggestions = + state.suggestions.whereNot((element) => state.searchHistory.contains(element)); + final historyMatches = state.searchHistory.where((element) => element.startsWith(query)); + for (var match in historyMatches.take(5)) { + yield ListTile( + title: Text(match), + leading: const Icon(Icons.history), + onLongPress: () => _onDeleteHistoryEntry(match), + onTap: () => _selectSuggestion(match), + trailing: _buildInsertSuggestionButton(match), + ); + } + + for (var suggestion in suggestions) { + yield ListTile( + title: Text(suggestion), + leading: const Icon(Icons.search), + onTap: () => _selectSuggestion(suggestion), + trailing: _buildInsertSuggestionButton(suggestion), + ); + } + } + + void _onDeleteHistoryEntry(String entry) async { + final shouldRemove = await showDialog( + context: context, + builder: (context) => RemoveHistoryEntryDialog(entry: entry), + ) ?? + false; + if (shouldRemove) { + context.read().removeHistoryEntry(entry); + } + } + + Widget _buildInsertSuggestionButton(String suggestion) { + return Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: IconButton( + icon: const Icon(Icons.arrow_outward), + onPressed: () { + _controller.text = '$suggestion '; + _controller.selection = TextSelection.fromPosition( + TextPosition(offset: _controller.text.length), + ); + }, + ), + ); + } + + Widget _buildResultsView(DocumentSearchState state) { + final header = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context)!.results, + style: Theme.of(context).textTheme.bodySmall, + ), + BlocBuilder( + builder: (context, state) { + return ViewTypeSelectionWidget( + viewType: state.viewType, + onChanged: (type) => context.read().updateViewType(type), + ); + }, + ) + ], + ).padded(); + return CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: header), + if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) + SliverToBoxAdapter( + child: Center( + child: Text(S.of(context)!.noMatchesFound), + ), + ) + else + SliverAdaptiveDocumentsView( + viewType: state.viewType, + documents: state.documents, + hasInternetConnection: true, + isLabelClickable: false, + isLoading: state.isLoading, + hasLoaded: state.hasLoaded, + enableHeroAnimation: false, + onTap: (document) { + pushDocumentDetailsRoute( + context, + document: document, + isLabelClickable: false, + ); + }, + ) + ], + ); + } + + void _selectSuggestion(String suggestion) { + _controller.text = suggestion; + context.read().search(suggestion); + FocusScope.of(context).unfocus(); + } +} diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 131393d..a0ec9ee 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -13,7 +13,6 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'dart:math' as math; - class DocumentSearchPage extends StatefulWidget { const DocumentSearchPage({super.key}); @@ -211,10 +210,6 @@ class _DocumentSearchPageState extends State { isLabelClickable: false, ); }, - correspondents: state.correspondents, - documentTypes: state.documentTypes, - tags: state.tags, - storagePaths: state.storagePaths, ) ], ); diff --git a/lib/features/document_search/view/documents_search_app_bar.dart b/lib/features/document_search/view/documents_search_app_bar.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/features/document_search/view/documents_search_app_bar.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart index 044dbdc..d39b41d 100644 --- a/lib/features/document_search/view/sliver_search_bar.dart +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s; +import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; @@ -23,6 +28,8 @@ class SliverSearchBar extends StatelessWidget { @override Widget build(BuildContext context) { + final currentUser = + Hive.box(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; return SliverPersistentHeader( floating: floating, pinned: pinned, @@ -30,45 +37,56 @@ class SliverSearchBar extends StatelessWidget { minExtent: kToolbarHeight, maxExtent: kToolbarHeight, child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - child: s.SearchBar( - height: kToolbarHeight, - supportingText: S.of(context)!.searchDocuments, - onTap: () => pushDocumentSearchPage(context), - leadingIcon: IconButton( - icon: const Icon(Icons.menu), - onPressed: Scaffold.of(context).openDrawer, - ), - trailingIcon: IconButton( - icon: GlobalSettingsBuilder( - builder: (context, settings) { - return ValueListenableBuilder( - valueListenable: - Hive.box(HiveBoxes.localUserAccount).listenable(), - builder: (context, box, _) { - final account = box.get(settings.currentLoggedInUser!)!; - return UserAvatar( - userId: settings.currentLoggedInUser!, - account: account, - ); - }, - ); - }, - ), - onPressed: () { - final apiVersion = context.read(); - showDialog( - context: context, - builder: (context) => Provider.value( - value: apiVersion, - child: const ManageAccountsPage(), - ), - ); - }, + margin: const EdgeInsets.symmetric(horizontal: 16.0), + child: BlocProvider( + create: (context) => DocumentSearchCubit( + context.read(), + context.read(), + context.read(), + Hive.box(HiveBoxes.localUserAppState).get(currentUser)!, ), + child: const DocumentSearchBar(), ), ), ), ); } + + s.SearchBar _buildOld(BuildContext context) { + return s.SearchBar( + height: kToolbarHeight, + supportingText: S.of(context)!.searchDocuments, + onTap: () => pushDocumentSearchPage(context), + leadingIcon: IconButton( + icon: const Icon(Icons.menu), + onPressed: Scaffold.of(context).openDrawer, + ), + trailingIcon: IconButton( + icon: GlobalSettingsBuilder( + builder: (context, settings) { + return ValueListenableBuilder( + valueListenable: Hive.box(HiveBoxes.localUserAccount).listenable(), + builder: (context, box, _) { + final account = box.get(settings.currentLoggedInUser!)!; + return UserAvatar( + userId: settings.currentLoggedInUser!, + account: account, + ); + }, + ); + }, + ), + onPressed: () { + final apiVersion = context.read(); + showDialog( + context: context, + builder: (context) => Provider.value( + value: apiVersion, + child: const ManageAccountsPage(), + ), + ); + }, + ), + ); + } } diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 3e2199a..fc2c399 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -333,10 +333,6 @@ class _DocumentsPageState extends State with SingleTickerProvider isLabelClickable: true, isLoading: state.isLoading, selectedDocumentIds: state.selectedIds, - correspondents: state.correspondents, - documentTypes: state.documentTypes, - tags: state.tags, - storagePaths: state.storagePaths, ); }, ), diff --git a/lib/features/documents/view/widgets/adaptive_documents_view.dart b/lib/features/documents/view/widgets/adaptive_documents_view.dart index 6241a98..47f9110 100644 --- a/lib/features/documents/view/widgets/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/adaptive_documents_view.dart @@ -24,11 +24,6 @@ abstract class AdaptiveDocumentsView extends StatelessWidget { final void Function(int? id)? onDocumentTypeSelected; final void Function(int? id)? onStoragePathSelected; - final Map correspondents; - final Map documentTypes; - final Map tags; - final Map storagePaths; - bool get showLoadingPlaceholder => !hasLoaded && isLoading; const AdaptiveDocumentsView({ @@ -47,10 +42,6 @@ abstract class AdaptiveDocumentsView extends StatelessWidget { required this.isLoading, required this.hasLoaded, this.enableHeroAnimation = true, - required this.correspondents, - required this.documentTypes, - required this.tags, - required this.storagePaths, }); AdaptiveDocumentsView.fromPagedState( @@ -67,10 +58,6 @@ abstract class AdaptiveDocumentsView extends StatelessWidget { required this.hasInternetConnection, this.viewType = ViewType.list, this.selectedDocumentIds = const [], - required this.correspondents, - required this.documentTypes, - required this.tags, - required this.storagePaths, }) : documents = state.documents, isLoading = state.isLoading, hasLoaded = state.hasLoaded; @@ -93,10 +80,6 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { super.enableHeroAnimation, required super.isLoading, required super.hasLoaded, - required super.correspondents, - required super.documentTypes, - required super.tags, - required super.storagePaths, }); @override @@ -132,10 +115,6 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { onDocumentTypeSelected: onDocumentTypeSelected, onStoragePathSelected: onStoragePathSelected, enableHeroAnimation: enableHeroAnimation, - correspondents: correspondents, - documentTypes: documentTypes, - storagePaths: storagePaths, - tags: tags, ); }, ), @@ -165,10 +144,6 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { onStoragePathSelected: onStoragePathSelected, enableHeroAnimation: enableHeroAnimation, highlights: document.searchHit?.highlights, - correspondents: correspondents, - documentTypes: documentTypes, - storagePaths: storagePaths, - tags: tags, ); }, ), @@ -201,10 +176,6 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { onDocumentTypeSelected: onDocumentTypeSelected, onStoragePathSelected: onStoragePathSelected, enableHeroAnimation: enableHeroAnimation, - correspondents: correspondents, - documentTypes: documentTypes, - storagePaths: storagePaths, - tags: tags, ); }, ); @@ -230,10 +201,6 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { super.selectedDocumentIds, super.viewType, super.enableHeroAnimation = true, - required super.correspondents, - required super.documentTypes, - required super.tags, - required super.storagePaths, }); @override @@ -272,10 +239,6 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { onDocumentTypeSelected: onDocumentTypeSelected, onStoragePathSelected: onStoragePathSelected, enableHeroAnimation: enableHeroAnimation, - correspondents: correspondents, - documentTypes: documentTypes, - storagePaths: storagePaths, - tags: tags, ); }, ); @@ -306,10 +269,6 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { onDocumentTypeSelected: onDocumentTypeSelected, onStoragePathSelected: onStoragePathSelected, enableHeroAnimation: enableHeroAnimation, - correspondents: correspondents, - documentTypes: documentTypes, - storagePaths: storagePaths, - tags: tags, ); }, ); @@ -344,10 +303,6 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { onDocumentTypeSelected: onDocumentTypeSelected, onStoragePathSelected: onStoragePathSelected, enableHeroAnimation: enableHeroAnimation, - correspondents: correspondents, - documentTypes: documentTypes, - storagePaths: storagePaths, - tags: tags, ); }, ); diff --git a/lib/features/documents/view/widgets/items/document_detailed_item.dart b/lib/features/documents/view/widgets/items/document_detailed_item.dart index bda4953..5fd6ae9 100644 --- a/lib/features/documents/view/widgets/items/document_detailed_item.dart +++ b/lib/features/documents/view/widgets/items/document_detailed_item.dart @@ -3,12 +3,14 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:intl/intl.dart'; +import 'package:paperless_mobile/core/repository/label_repository.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/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:provider/provider.dart'; class DocumentDetailedItem extends DocumentItem { final String? highlights; @@ -26,10 +28,6 @@ class DocumentDetailedItem extends DocumentItem { super.onStoragePathSelected, super.onTagSelected, super.onTap, - required super.tags, - required super.correspondents, - required super.documentTypes, - required super.storagePaths, }); @override @@ -116,7 +114,8 @@ class DocumentDetailedItem extends DocumentItem { textStyle: Theme.of(context).textTheme.titleSmall?.apply( color: Theme.of(context).colorScheme.onSurfaceVariant, ), - correspondent: correspondents[document.correspondent], + correspondent: + context.watch().state.correspondents[document.correspondent], ), ], ).paddedLTRB(8, 0, 8, 4), @@ -131,13 +130,16 @@ class DocumentDetailedItem extends DocumentItem { textStyle: Theme.of(context).textTheme.titleSmall?.apply( color: Theme.of(context).colorScheme.onSurfaceVariant, ), - documentType: documentTypes[document.documentType], + documentType: + context.watch().state.documentTypes[document.documentType], ), ], ).paddedLTRB(8, 0, 8, 4), TagsWidget( isMultiLine: false, - tags: document.tags.map((e) => tags[e]!).toList(), + tags: document.tags + .map((e) => context.watch().state.tags[e]!) + .toList(), ).padded(), if (highlights != null) Html( diff --git a/lib/features/documents/view/widgets/items/document_grid_item.dart b/lib/features/documents/view/widgets/items/document_grid_item.dart index 506acbe..e7a954c 100644 --- a/lib/features/documents/view/widgets/items/document_grid_item.dart +++ b/lib/features/documents/view/widgets/items/document_grid_item.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:paperless_mobile/core/repository/label_repository.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'; +import 'package:provider/provider.dart'; class DocumentGridItem extends DocumentItem { const DocumentGridItem({ @@ -20,10 +22,6 @@ class DocumentGridItem extends DocumentItem { super.onTagSelected, super.onTap, required super.enableHeroAnimation, - required super.tags, - required super.correspondents, - required super.documentTypes, - required super.storagePaths, }); @override @@ -32,9 +30,8 @@ class DocumentGridItem extends DocumentItem { padding: const EdgeInsets.all(8.0), child: Card( elevation: 1.0, - color: isSelected - ? Theme.of(context).colorScheme.inversePrimary - : Theme.of(context).cardColor, + color: + isSelected ? Theme.of(context).colorScheme.inversePrimary : Theme.of(context).cardColor, child: InkWell( borderRadius: BorderRadius.circular(12), onTap: _onTap, @@ -57,10 +54,16 @@ class DocumentGridItem extends DocumentItem { crossAxisAlignment: CrossAxisAlignment.start, children: [ CorrespondentWidget( - correspondent: correspondents[document.correspondent], + correspondent: context + .watch() + .state + .correspondents[document.correspondent], ), DocumentTypeWidget( - documentType: documentTypes[document.documentType], + documentType: context + .watch() + .state + .documentTypes[document.documentType], ), Text( document.title, @@ -70,7 +73,9 @@ class DocumentGridItem extends DocumentItem { ), const Spacer(), TagsWidget( - tags: document.tags.map((e) => tags[e]!).toList(), + tags: document.tags + .map((e) => context.watch().state.tags[e]!) + .toList(), isMultiLine: false, onTagSelected: onTagSelected, ), diff --git a/lib/features/documents/view/widgets/items/document_item.dart b/lib/features/documents/view/widgets/items/document_item.dart index bf29f45..a19fef3 100644 --- a/lib/features/documents/view/widgets/items/document_item.dart +++ b/lib/features/documents/view/widgets/items/document_item.dart @@ -10,11 +10,6 @@ abstract class DocumentItem extends StatelessWidget { final bool isLabelClickable; final bool enableHeroAnimation; - final Map tags; - final Map correspondents; - final Map documentTypes; - final Map storagePaths; - final void Function(int tagId)? onTagSelected; final void Function(int? correspondentId)? onCorrespondentSelected; final void Function(int? documentTypeId)? onDocumentTypeSelected; @@ -33,9 +28,5 @@ abstract class DocumentItem extends StatelessWidget { this.onDocumentTypeSelected, this.onStoragePathSelected, required this.enableHeroAnimation, - required this.tags, - required this.correspondents, - required this.documentTypes, - required this.storagePaths, }); } 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 bf16352..b08df94 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:paperless_mobile/core/repository/label_repository.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'; +import 'package:provider/provider.dart'; class DocumentListItem extends DocumentItem { static const _a4AspectRatio = 1 / 1.4142; @@ -21,14 +23,11 @@ class DocumentListItem extends DocumentItem { super.onTagSelected, super.onTap, super.enableHeroAnimation = true, - required super.tags, - required super.correspondents, - required super.documentTypes, - required super.storagePaths, }); @override Widget build(BuildContext context) { + final labels = context.watch().state; return Material( child: ListTile( dense: true, @@ -46,7 +45,10 @@ class DocumentListItem extends DocumentItem { absorbing: isSelectionActive, child: CorrespondentWidget( isClickable: isLabelClickable, - correspondent: correspondents[document.correspondent], + correspondent: context + .watch() + .state + .correspondents[document.correspondent], onSelected: onCorrespondentSelected, ), ), @@ -62,8 +64,8 @@ class DocumentListItem extends DocumentItem { child: TagsWidget( isClickable: isLabelClickable, tags: document.tags - .where((e) => tags.containsKey(e)) - .map((e) => tags[e]!) + .where((e) => labels.tags.containsKey(e)) + .map((e) => labels.tags[e]!) .toList(), isMultiLine: false, onTagSelected: (id) => onTagSelected?.call(id), @@ -78,15 +80,12 @@ class DocumentListItem extends DocumentItem { overflow: TextOverflow.ellipsis, text: TextSpan( text: DateFormat.yMMMd().format(document.created), - style: Theme.of(context) - .textTheme - .labelSmall - ?.apply(color: Colors.grey), + style: Theme.of(context).textTheme.labelSmall?.apply(color: Colors.grey), children: document.documentType != null ? [ const TextSpan(text: '\u30FB'), TextSpan( - text: documentTypes[document.documentType]?.name, + text: labels.documentTypes[document.documentType]?.name, ), ] : null, diff --git a/lib/features/linked_documents/view/linked_documents_page.dart b/lib/features/linked_documents/view/linked_documents_page.dart index 70c1bf2..889da31 100644 --- a/lib/features/linked_documents/view/linked_documents_page.dart +++ b/lib/features/linked_documents/view/linked_documents_page.dart @@ -8,7 +8,6 @@ import 'package:paperless_mobile/features/linked_documents/cubit/linked_document import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; - class LinkedDocumentsPage extends StatefulWidget { const LinkedDocumentsPage({super.key}); @@ -58,10 +57,6 @@ class _LinkedDocumentsPageState extends State isLabelClickable: false, ); }, - correspondents: state.correspondents, - documentTypes: state.documentTypes, - storagePaths: state.storagePaths, - tags: state.tags, ), ], ); diff --git a/lib/features/saved_view_details/view/saved_view_details_page.dart b/lib/features/saved_view_details/view/saved_view_details_page.dart index d19e0e1..d82bd21 100644 --- a/lib/features/saved_view_details/view/saved_view_details_page.dart +++ b/lib/features/saved_view_details/view/saved_view_details_page.dart @@ -83,10 +83,6 @@ class _SavedViewDetailsPageState extends State ); }, viewType: state.viewType, - correspondents: state.correspondents, - documentTypes: state.documentTypes, - tags: state.tags, - storagePaths: state.storagePaths, ), if (state.hasLoaded && state.isLoading) const SliverToBoxAdapter( diff --git a/lib/features/similar_documents/view/similar_documents_view.dart b/lib/features/similar_documents/view/similar_documents_view.dart index a12a466..9cf7a65 100644 --- a/lib/features/similar_documents/view/similar_documents_view.dart +++ b/lib/features/similar_documents/view/similar_documents_view.dart @@ -66,10 +66,6 @@ class _SimilarDocumentsViewState extends State isLabelClickable: false, ); }, - correspondents: state.correspondents, - documentTypes: state.documentTypes, - tags: state.tags, - storagePaths: state.storagePaths, ); }, ); diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index 7421f2d..73637bd 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -29,3 +29,4 @@ export 'permissions/user_permissions.dart'; export 'permissions/inherited_permissions.dart'; export 'group_model.dart'; export 'user_model.dart'; +export 'permissions/user_permission_extension.dart'; diff --git a/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart b/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart new file mode 100644 index 0000000..e1c8d8f --- /dev/null +++ b/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart @@ -0,0 +1,14 @@ +import 'package:paperless_api/paperless_api.dart'; + +extension UserPermissionExtension on UserModel { + bool hasPermission(PermissionAction action, PermissionTarget target) { + return map( + v3: (user) { + final permission = [action.value, target.value].join("_"); + return user.userPermissions.any((element) => element == permission) || + user.inheritedPermissions.any((element) => element.split(".").last == permission); + }, + v2: (_) => true, + ); + } +} diff --git a/packages/paperless_api/lib/src/models/user_model.dart b/packages/paperless_api/lib/src/models/user_model.dart index 2d72ac0..340f5f4 100644 --- a/packages/paperless_api/lib/src/models/user_model.dart +++ b/packages/paperless_api/lib/src/models/user_model.dart @@ -3,7 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; import 'package:paperless_api/config/hive/hive_type_ids.dart'; -import 'package:paperless_api/src/models/permissions/user_permissions.dart'; part 'user_model.freezed.dart'; part 'user_model.g.dart'; @@ -42,29 +41,17 @@ class UserModel with _$UserModel { String? get fullName => map( v2: (value) => value.displayName, v3: (value) { - if (value.firstName == null && value.lastName == null) { + bool hasFirstName = value.firstName?.trim().isNotEmpty ?? false; + bool hasLastName = value.lastName?.trim().isNotEmpty ?? false; + if (hasFirstName && hasLastName) { + return "${value.firstName!} ${value.lastName!}"; + } else if (hasFirstName) { + return value.firstName!; + } else if (hasLastName) { + return value.lastName; + } else { return null; } - if (value.firstName == null) { - return value.lastName; - } - return "${value.firstName!} ${value.lastName ?? ''}"; }, ); - - bool hasPermission(PermissionAction action, PermissionTarget target) { - return map( - v3: (value) { - if (value.isSuperuser) { - return true; - } - final permissionIdentifier = "${action.value}_${target.value}"; - return value.userPermissions.contains(permissionIdentifier); - }, - v2: (value) { - // In previous versions, all users have all permissions. - return true; - }, - ); - } } diff --git a/packages/paperless_document_scanner/example/android/build.gradle b/packages/paperless_document_scanner/example/android/build.gradle index 58a8c74..713d7f6 100644 --- a/packages/paperless_document_scanner/example/android/build.gradle +++ b/packages/paperless_document_scanner/example/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/paperless_document_scanner/example/lib/main.dart b/packages/paperless_document_scanner/example/lib/main.dart index 1f514bc..ba0240a 100644 --- a/packages/paperless_document_scanner/example/lib/main.dart +++ b/packages/paperless_document_scanner/example/lib/main.dart @@ -1,8 +1,9 @@ - import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image/image.dart' as imglib; +import 'package:camerawesome/camerawesome_plugin.dart'; +import 'package:path_provider/path_provider.dart'; late final List cameras; void main() async { @@ -19,27 +20,9 @@ class EdgeDetectionApp extends StatefulWidget { } class _EdgeDetectionAppState extends State { - CameraImage? _image; - late final CameraController _controller; - @override void initState() { super.initState(); - - () async { - _controller = CameraController( - cameras - .where( - (element) => element.lensDirection == CameraLensDirection.back) - .first, - ResolutionPreset.low, - enableAudio: false, - ); - await _controller.initialize(); - _controller.startImageStream((image) { - setState(() => _image = image); - }); - }(); } Uint8List concatenatePlanes(List planes) { @@ -68,8 +51,7 @@ class _EdgeDetectionAppState extends State { // Fill image buffer with plane[0] from YUV420_888 for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { - final int uvIndex = - uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor(); + final int uvIndex = uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor(); final int index = y * width + x; final yp = image.planes[0].bytes[index]; @@ -77,9 +59,7 @@ class _EdgeDetectionAppState extends State { final vp = image.planes[2].bytes[uvIndex]; // Calculate pixel color int r = (yp + vp * 1436 / 1024 - 179).round().clamp(0, 255); - int g = (yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91) - .round() - .clamp(0, 255); + int g = (yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91).round().clamp(0, 255); int b = (yp + up * 1814 / 1024 - 227).round().clamp(0, 255); // color: 0x FF FF FF FF // A B G R @@ -100,10 +80,22 @@ class _EdgeDetectionAppState extends State { visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Scaffold( - body: Center( - child: _image != null - ? convertYUV420toImageColor(_image!) - : const Placeholder(), + body: CameraAwesomeBuilder.awesome( + saveConfig: SaveConfig.photo( + pathBuilder: () => + getApplicationDocumentsDirectory().then((value) => "${value.path}/test.jpg"), + ), + onImageForAnalysis: (image) async {}, + imageAnalysisConfig: AnalysisConfig( + // Android specific options + androidOptions: const AndroidAnalysisOptions.yuv420( + // Target width (CameraX will chose the closest resolution to this width) + width: 250, + ), + // Wether to start automatically the analysis (true by default) + autoStart: true, + // Max frames per second, null for no limit (default) + ), ), ), ); diff --git a/packages/paperless_document_scanner/example/pubspec.lock b/packages/paperless_document_scanner/example/pubspec.lock index 37fac4d..6b2cc09 100644 --- a/packages/paperless_document_scanner/example/pubspec.lock +++ b/packages/paperless_document_scanner/example/pubspec.lock @@ -65,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1+3" + camerawesome: + dependency: transitive + description: + name: camerawesome + sha256: "0fd4ad7cf85ced7c3018edbe5a66d8c4b630ada7120b02c01961db3ea36f52ce" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + carousel_slider: + dependency: transitive + description: + name: carousel_slider + sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42" + url: "https://pub.dev" + source: hosted + version: "4.2.1" characters: dependency: transitive description: @@ -89,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.1" + colorfilter_generator: + dependency: transitive + description: + name: colorfilter_generator + sha256: ccc2995e440b1d828d55d99150e7cad64624f3cb4a1e235000de3f93cf10d35c + url: "https://pub.dev" + source: hosted + version: "0.0.8" convert: dependency: transitive description: @@ -216,6 +240,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + matrix2d: + dependency: transitive + description: + name: matrix2d + sha256: "188718dd3bc2a31e372cfd0791b0f77f4f13ea76164147342cc378d9132949e7" + url: "https://pub.dev" + source: hosted + version: "1.0.4" meta: dependency: transitive description: @@ -335,6 +367,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" sky_engine: dependency: transitive description: flutter diff --git a/packages/paperless_document_scanner/pubspec.yaml b/packages/paperless_document_scanner/pubspec.yaml index 2debf35..8c3a641 100644 --- a/packages/paperless_document_scanner/pubspec.yaml +++ b/packages/paperless_document_scanner/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: ">=2.5.0" dependencies: + camerawesome: ^1.4.0 ffi: ^2.0.1 flutter: sdk: flutter