WIP - Added document search, restructured navigation

This commit is contained in:
Anton Stubenbord
2023-01-24 00:38:37 +01:00
parent f6ecbae6e8
commit e68e3af713
15 changed files with 970 additions and 126 deletions

View File

@@ -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<DocumentSearchState>
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<void> 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<void> 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<List<String>> findSuggestions(String query) {
return api.autocomplete(query);
}
@override

View File

@@ -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<String> searchHistory;
final List<String> 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<String, dynamic> toJson() => _$DocumentSearchStateToJson(this);
}
class

View File

@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'document_search_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DocumentSearchState _$DocumentSearchStateFromJson(Map<String, dynamic> json) =>
DocumentSearchState(
searchHistory: (json['searchHistory'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$DocumentSearchStateToJson(
DocumentSearchState instance) =>
<String, dynamic>{
'searchHistory': instance.searchHistory,
};

View File

@@ -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<DocumentModel> {
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<DocumentModel> {
final DocumentSearchCubit bloc;
DocumentSearchDelegate(
this.bloc, {
required String hintText,
required super.searchFieldStyle,
}) : super(
@@ -23,60 +29,141 @@ class DocumentSearchDelegate extends SearchDelegate<DocumentModel> {
@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<DocumentSearchCubit, DocumentSearchState>(
return BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
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<PaperlessDocumentsApi>().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<List<String>>(
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<PaperlessDocumentsApi>()
.findAll(DocumentFilter(query: TextQuery.titleAndContent(query))),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
return BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
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<DocumentModel?>(
context,
@@ -102,5 +189,18 @@ class DocumentSearchDelegate extends SearchDelegate<DocumentModel> {
}
@override
List<Widget> buildActions(BuildContext context) => <Widget>[];
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
icon: Icon(
Icons.clear,
color: Theme.of(context).colorScheme.onSurfaceVariant,
).paddedSymmetrically(horizontal: 16),
onPressed: () {
query = '';
super.showSuggestions(context);
},
),
];
}
}

View File

@@ -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)})",
// ),
);
}
}