diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart index 74dd3a1..4f10822 100644 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -87,6 +87,10 @@ class DocumentSearchCubit extends Cubit with DocumentPaging } Future suggest(String query) async { + final normalizedQuery = query.trim(); + if (normalizedQuery.isEmpty) { + return; + } emit( state.copyWith( isLoading: true, @@ -96,10 +100,13 @@ class DocumentSearchCubit extends Cubit with DocumentPaging ), ); final suggestions = await api.autocomplete(query); - emit(state.copyWith( - suggestions: suggestions, - isLoading: false, - )); + print("Suggestions found: $suggestions"); + emit( + state.copyWith( + suggestions: suggestions, + isLoading: false, + ), + ); } void reset() { diff --git a/lib/features/document_search/view/document_search_bar.dart b/lib/features/document_search/view/document_search_bar.dart index b13f0e5..85f591b 100644 --- a/lib/features/document_search/view/document_search_bar.dart +++ b/lib/features/document_search/view/document_search_bar.dart @@ -1,18 +1,15 @@ -import 'dart:async'; -import 'dart:math' as math; - -import 'package:collection/collection.dart'; +import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_api/paperless_api.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/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/user_repository.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/document_search/view/document_search_page.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'; @@ -28,280 +25,113 @@ class DocumentSearchBar extends StatefulWidget { } class _DocumentSearchBarState extends State { - Timer? _debounceTimer; - - final _controller = SearchController(); - - @override - void initState() { - super.initState(); - _controller.addListener(() { - _debounceTimer?.cancel(); - _debounceTimer = Timer(const Duration(milliseconds: 500), () { - print("Searching for $query"); - context.read().suggest(query); - }); - }); - } - - String get query => _controller.text; - @override Widget build(BuildContext context) { - return Theme( - data: Theme.of(context).copyWith( - inputDecorationTheme: const InputDecorationTheme(), + return OpenContainer( + transitionType: ContainerTransitionType.fadeThrough, + closedElevation: 1, + middleColor: Theme.of(context).colorScheme.surfaceVariant, + openColor: Theme.of(context).colorScheme.background, + closedColor: Theme.of(context).colorScheme.surfaceVariant, + closedShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(56), ), - child: BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.only(top: 4), - child: SearchAnchor( - searchController: _controller, - viewHintText: S.of(context)!.searchDocuments, - builder: (context, controller) { - return SearchBar( - focusNode: FocusNode(), - controller: controller, - leading: IconButton( - icon: const Icon(Icons.menu), - onPressed: Scaffold.of(context).openDrawer, - ), - trailing: [ - 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(), + closedBuilder: (_, action) { + return InkWell( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 720, + minWidth: 360, + maxHeight: 56, + minHeight: 48, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + IconButton( + icon: Icon(Icons.menu), + onPressed: Scaffold.of(context).openDrawer, + ), + Expanded( + child: Hero( + tag: "search_hero_tag", + child: TextField( + enabled: false, + decoration: InputDecoration( + border: InputBorder.none, + hintText: S.of(context)!.searchDocuments, + hintStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), ), - ); - }, + ), + ], ), - ], - hintText: S.of(context)!.searchDocuments, - onTap: () { - controller.openView().then((value) => FocusScope.of(context).unfocus()); - }, - ); - }, - suggestionsBuilder: (context, controller) { - switch (state.view) { - case SearchView.suggestions: - return _buildSuggestionItems(state); - case SearchView.results: - return _buildResultsList(state); - } - }, - ), - ); - - return SearchAnchor.bar( - barPadding: MaterialStatePropertyAll(EdgeInsets.only(left: 8, right: 0)), - viewLeading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - // FocusManager.instance.primaryFocus?.unfocus(); - _controller.clear(); - _controller.closeView(null); - Future.delayed(const Duration(milliseconds: 100), () { - FocusManager.instance.primaryFocus?.unfocus(); - }); - }, - ), - 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 (state.view) { - case SearchView.suggestions: - return _buildSuggestionItems(state); - case SearchView.results: - return _buildResultsList(state); - } - }, - ); - }, - ), - ); - } - - 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), - ); - }, - ), - ); - } - - Iterable _buildResultsList(DocumentSearchState state) sync* { - if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { - yield Center( - child: Text(S.of(context)!.noMatchesFound), - ); - return; - } - yield DefaultAdaptiveDocumentsView( - 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, + _buildUserAvatar(context), + ], + ), + ), + ); + }, + openBuilder: (_, action) { + return MultiProvider( + providers: [ + Provider.value(value: context.read()), + Provider.value(value: context.read()), + Provider.value(value: context.read()), + Provider.value(value: context.read()), + Provider.value(value: context.read()), + ], + child: Provider( + create: (_) => DocumentSearchCubit( + context.read(), + context.read(), + context.read(), + Hive.box(HiveBoxes.localUserAppState) + .get(LocalUserAccount.current.id)!, + ), + builder: (_, __) => const DocumentSearchPage(), + ), ); }, ); } - 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, + IconButton _buildUserAvatar(BuildContext context) { + return 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(), + ), + ); + }, ); } - - 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 a0ec9ee..5bd6c99 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -32,38 +32,38 @@ class _DocumentSearchPageState extends State { final theme = Theme.of(context); return Scaffold( appBar: AppBar( - backgroundColor: theme.colorScheme.surface, + backgroundColor: theme.colorScheme.surfaceVariant, toolbarHeight: 72, leading: BackButton( - color: theme.colorScheme.onSurface, + color: theme.colorScheme.onSurfaceVariant, ), - title: TextField( - autofocus: true, - style: theme.textTheme.bodyLarge?.apply( - color: theme.colorScheme.onSurface, - ), - focusNode: _queryFocusNode, - decoration: InputDecoration( - contentPadding: EdgeInsets.zero, - hintStyle: theme.textTheme.bodyLarge?.apply( - color: theme.colorScheme.onSurfaceVariant, + title: Hero( + tag: "search_hero_tag", + child: TextField( + autofocus: true, + // style: theme.textTheme.bodyLarge?.apply( + // color: theme.colorScheme.onSurface, + // ), + focusNode: _queryFocusNode, + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + hintText: S.of(context)!.searchDocuments, + border: InputBorder.none, ), - hintText: S.of(context)!.searchDocuments, - border: InputBorder.none, + controller: _queryController, + onChanged: (query) { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(milliseconds: 500), () { + context.read().suggest(query); + }); + }, + textInputAction: TextInputAction.search, + onSubmitted: (query) { + FocusScope.of(context).unfocus(); + _debounceTimer?.cancel(); + context.read().search(query); + }, ), - controller: _queryController, - onChanged: (query) { - _debounceTimer?.cancel(); - _debounceTimer = Timer(const Duration(milliseconds: 500), () { - context.read().suggest(query); - }); - }, - textInputAction: TextInputAction.search, - onSubmitted: (query) { - FocusScope.of(context).unfocus(); - _debounceTimer?.cancel(); - context.read().search(query); - }, ), actions: [ IconButton( @@ -75,22 +75,22 @@ class _DocumentSearchPageState extends State { }, ).padded(), ], - bottom: PreferredSize( - preferredSize: const 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); - } - }, + body: Column( + children: [ + Expanded( + child: BlocBuilder( + builder: (context, state) { + switch (state.view) { + case SearchView.suggestions: + return _buildSuggestionsView(state); + case SearchView.results: + return _buildResultsView(state); + } + }, + ), + ), + ], ), ); } @@ -134,7 +134,7 @@ class _DocumentSearchPageState extends State { ), childCount: suggestions.length, ), - ) + ), ], ); } diff --git a/lib/features/documents/view/widgets/document_preview.dart b/lib/features/documents/view/widgets/document_preview.dart index 74da008..11d2e6d 100644 --- a/lib/features/documents/view/widgets/document_preview.dart +++ b/lib/features/documents/view/widgets/document_preview.dart @@ -43,9 +43,7 @@ class DocumentPreview extends StatelessWidget { fit: fit, alignment: alignment, cacheKey: "thumb_${document.id}", - imageUrl: context - .read() - .getThumbnailUrl(document.id), + imageUrl: context.read().getThumbnailUrl(document.id), errorWidget: (ctxt, msg, __) => Text(msg), placeholder: (context, value) => Shimmer.fromColors( baseColor: Colors.grey[300]!, diff --git a/packages/paperless_document_scanner/example/lib/camera_view.dart b/packages/paperless_document_scanner/example/lib/camera_view.dart deleted file mode 100644 index 8eae9b6..0000000 --- a/packages/paperless_document_scanner/example/lib/camera_view.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:camera/camera.dart'; -import 'package:flutter/material.dart'; - -class CameraView extends StatelessWidget { - const CameraView({super.key, required this.controller}); - - final CameraController controller; - - @override - Widget build(BuildContext context) { - if (!controller.value.isInitialized) { - return Container(); - } - - return Center( - child: CameraPreview(controller), - ); - } -} diff --git a/packages/paperless_document_scanner/example/lib/scan.dart b/packages/paperless_document_scanner/example/lib/scan.dart index cfacfb8..aed8a26 100644 --- a/packages/paperless_document_scanner/example/lib/scan.dart +++ b/packages/paperless_document_scanner/example/lib/scan.dart @@ -5,7 +5,6 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:paperless_document_scanner/types/edge_detection_result.dart'; -import 'camera_view.dart'; import 'cropping_preview.dart'; import 'edge_detector.dart'; import 'image_view.dart'; @@ -65,10 +64,6 @@ class _ScanState extends State { return ImageView(imagePath: croppedImagePath!); } - if (imagePath == null && edgeDetectionResult == null) { - return CameraView(controller: controller); - } - return ImagePreview( imagePath: imagePath!, edgeDetectionResult: edgeDetectionResult, @@ -129,22 +124,19 @@ class _ScanState extends State { imagePath = filePath; }); - EdgeDetectionResult result = - await EdgeDetector().detectEdgesFromFile(filePath); + EdgeDetectionResult result = await EdgeDetector().detectEdgesFromFile(filePath); setState(() { edgeDetectionResult = result; }); } - Future _processImage( - String filePath, EdgeDetectionResult edgeDetectionResult) async { + Future _processImage(String filePath, EdgeDetectionResult edgeDetectionResult) async { if (!mounted) { return; } - bool result = await EdgeDetector() - .processImageFromFile(filePath, edgeDetectionResult); + bool result = await EdgeDetector().processImageFromFile(filePath, edgeDetectionResult); if (result == false) { return;