Improved search, changed saved view display

This commit is contained in:
Anton Stubenbord
2023-01-31 00:29:07 +01:00
parent b697dc7d8d
commit e9e9fdc336
27 changed files with 1549 additions and 1016 deletions

View File

@@ -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<FormBuilderState> 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<FormBuilderState> 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<DocumentFilterForm> createState() => _DocumentFilterFormState();
}
class _DocumentFilterFormState extends State<DocumentFilterForm> {
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<Widget> _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<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return LabelFormField<DocumentType>(
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<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent>(
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<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return LabelFormField<StoragePath>(
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<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, state) {
return TagFormField(
name: DocumentModel.tagsKey,
initialValue: widget.initialFilter.tags,
allowCreation: false,
selectableOptions: state.labels,
);
},
);
}
}

View File

@@ -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<DocumentFilterPanel> {
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<FormBuilderState>();
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<DocumentFilterPanel> {
),
),
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<Widget> _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<DocumentFilterPanel> {
);
}
BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
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<DocumentFilterPanel> {
);
}
Widget _buildDocumentTypeFormField() {
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return LabelFormField<DocumentType>(
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<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent>(
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<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return LabelFormField<StoragePath>(
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);
}
}
}