diff --git a/lib/core/widgets/dialog_utils/dialog_cancel_button.dart b/lib/core/widgets/dialog_utils/dialog_cancel_button.dart new file mode 100644 index 0000000..5cec505 --- /dev/null +++ b/lib/core/widgets/dialog_utils/dialog_cancel_button.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class DialogCancelButton extends StatelessWidget { + final void Function()? onTap; + const DialogCancelButton({super.key, this.onTap}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: Text(S.of(context).genericActionCancelLabel), + onPressed: onTap ?? () => Navigator.pop(context), + ); + } +} diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart index 8dba0c1..c07fb05 100644 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -3,7 +3,6 @@ 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/paged_document_view/cubit/document_paging_bloc_mixin.dart'; -import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; @@ -49,6 +48,16 @@ class DocumentSearchCubit extends HydratedCubit ); } + void removeHistoryEntry(String entry) { + emit( + state.copyWith( + searchHistory: state.searchHistory + .whereNot((element) => element == entry) + .toList(), + ), + ); + } + Future suggest(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 4bc96ab..e27841a 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -1,11 +1,15 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; 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/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/generated/l10n.dart'; import 'package:paperless_mobile/routes/document_details_route.dart'; +import 'dart:math' as math; Future showDocumentSearchPage(BuildContext context) { return Navigator.of(context).push( @@ -30,6 +34,9 @@ class DocumentSearchPage extends StatefulWidget { class _DocumentSearchPageState extends State { final _queryController = TextEditingController(text: ''); + final _queryFocusNode = FocusNode(); + + Timer? _debounceTimer; String get query => _queryController.text; @override @@ -47,6 +54,7 @@ class _DocumentSearchPageState extends State { style: theme.textTheme.bodyLarge?.apply( color: theme.colorScheme.onSurface, ), + focusNode: _queryFocusNode, decoration: InputDecoration( contentPadding: EdgeInsets.zero, hintStyle: theme.textTheme.bodyLarge?.apply( @@ -56,7 +64,12 @@ class _DocumentSearchPageState extends State { border: InputBorder.none, ), controller: _queryController, - onChanged: context.read().suggest, + onChanged: (query) { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(milliseconds: 700), () { + context.read().suggest(query); + }); + }, textInputAction: TextInputAction.search, onSubmitted: (query) { FocusScope.of(context).unfocus(); @@ -109,7 +122,9 @@ class _DocumentSearchPageState extends State { (context, index) => ListTile( title: Text(historyMatches[index]), leading: const Icon(Icons.history), + onLongPress: () => _onDeleteHistoryEntry(historyMatches[index]), onTap: () => _selectSuggestion(historyMatches[index]), + trailing: _buildInsertSuggestionButton(historyMatches[index]), ), childCount: historyMatches.length, ), @@ -127,6 +142,7 @@ class _DocumentSearchPageState extends State { title: Text(suggestions[index]), leading: const Icon(Icons.search), onTap: () => _selectSuggestion(suggestions[index]), + trailing: _buildInsertSuggestionButton(suggestions[index]), ), childCount: suggestions.length, ), @@ -135,6 +151,34 @@ class _DocumentSearchPageState extends State { ); } + 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: Icon(Icons.arrow_outward), + onPressed: () { + _queryController.text = '$suggestion '; + _queryController.selection = TextSelection.fromPosition( + TextPosition(offset: _queryController.text.length), + ); + _queryFocusNode.requestFocus(); + }, + ), + ); + } + Widget _buildResultsView(DocumentSearchState state) { final header = Text( S.of(context).documentSearchResults, diff --git a/lib/features/document_search/view/remove_history_entry_dialog.dart b/lib/features/document_search/view/remove_history_entry_dialog.dart new file mode 100644 index 0000000..13047a8 --- /dev/null +++ b/lib/features/document_search/view/remove_history_entry_dialog.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class RemoveHistoryEntryDialog extends StatelessWidget { + final String entry; + const RemoveHistoryEntryDialog({super.key, required this.entry}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(entry), + content: Text(S.of(context).documentSearchRemoveHistoryEntryText), + actions: [ + const DialogCancelButton(), + TextButton( + child: Text(S.of(context).genericActionRemoveLabel), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ); + } +} diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index c34f1d0..fe1d607 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -667,5 +667,7 @@ "viewTypeGridOption": "Grid", "@viewTypeGridOption": {}, "viewTypeListOption": "List", - "@viewTypeListOption": {} + "@viewTypeListOption": {}, + "genericActionRemoveLabel": "Remove", + "documentSearchRemoveHistoryEntryText": "Remove query from search history?" } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index ed36678..60a338e 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -667,5 +667,7 @@ "viewTypeGridOption": "Raster", "@viewTypeGridOption": {}, "viewTypeListOption": "Liste", - "@viewTypeListOption": {} + "@viewTypeListOption": {}, + "genericActionRemoveLabel": "Remove", + "documentSearchRemoveHistoryEntryText": "Remove query from search history?" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9d4c367..94b9c30 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -667,5 +667,7 @@ "viewTypeGridOption": "Grid", "@viewTypeGridOption": {}, "viewTypeListOption": "List", - "@viewTypeListOption": {} + "@viewTypeListOption": {}, + "genericActionRemoveLabel": "Remove", + "documentSearchRemoveHistoryEntryText": "Remove query from search history?" } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 579d742..7ef7bbe 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -667,5 +667,7 @@ "viewTypeGridOption": "Grid", "@viewTypeGridOption": {}, "viewTypeListOption": "List", - "@viewTypeListOption": {} + "@viewTypeListOption": {}, + "genericActionRemoveLabel": "Remove", + "documentSearchRemoveHistoryEntryText": "Remove query from search history?" } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index ae6b76d..5d3cf65 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -667,5 +667,7 @@ "viewTypeGridOption": "Grid", "@viewTypeGridOption": {}, "viewTypeListOption": "List", - "@viewTypeListOption": {} + "@viewTypeListOption": {}, + "genericActionRemoveLabel": "Remove", + "documentSearchRemoveHistoryEntryText": "Remove query from search history?" } \ No newline at end of file