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,232 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
abstract class AdaptiveDocumentsView extends StatelessWidget {
final List<DocumentModel> documents;
final bool isLoading;
final bool hasLoaded;
final bool enableHeroAnimation;
final List<int> selectedDocumentIds;
final ViewType viewType;
final void Function(DocumentModel)? onTap;
final void Function(DocumentModel)? onSelected;
final bool hasInternetConnection;
final bool isLabelClickable;
final void Function(int id)? onTagSelected;
final void Function(int? id)? onCorrespondentSelected;
final void Function(int? id)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
const AdaptiveDocumentsView({
super.key,
this.selectedDocumentIds = const [],
required this.documents,
this.onTap,
this.onSelected,
this.viewType = ViewType.list,
required this.hasInternetConnection,
required this.isLabelClickable,
this.onTagSelected,
this.onCorrespondentSelected,
this.onDocumentTypeSelected,
this.onStoragePathSelected,
required this.isLoading,
required this.hasLoaded,
this.enableHeroAnimation = true,
});
}
class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
const SliverAdaptiveDocumentsView({
super.key,
required super.documents,
required super.hasInternetConnection,
required super.isLabelClickable,
super.onCorrespondentSelected,
super.onDocumentTypeSelected,
super.onStoragePathSelected,
super.onSelected,
super.onTagSelected,
super.onTap,
super.selectedDocumentIds,
super.viewType,
required super.isLoading,
required super.hasLoaded,
});
@override
Widget build(BuildContext context) {
switch (viewType) {
case ViewType.grid:
return _buildGridView();
case ViewType.list:
return _buildListView();
}
}
Widget _buildListView() {
if (!hasLoaded && isLoading) {
return const DocumentsListLoadingWidget();
}
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: documents.length,
(context, index) {
final document = documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
),
);
},
),
);
}
Widget _buildGridView() {
if (!hasLoaded && isLoading) {
return const DocumentsListLoadingWidget();
}
return SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1 / 2,
),
itemCount: documents.length,
itemBuilder: (context, index) {
final document = documents.elementAt(index);
return DocumentGridItem(
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
isLabelClickable: isLabelClickable,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
);
},
);
}
}
class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
final ScrollController? scrollController;
const DefaultAdaptiveDocumentsView({
super.key,
required super.documents,
required super.hasInternetConnection,
required super.isLabelClickable,
required super.isLoading,
required super.hasLoaded,
super.onCorrespondentSelected,
super.onDocumentTypeSelected,
super.onStoragePathSelected,
super.onSelected,
super.onTagSelected,
super.onTap,
this.scrollController,
super.selectedDocumentIds,
super.viewType,
super.enableHeroAnimation = true,
});
@override
Widget build(BuildContext context) {
switch (viewType) {
case ViewType.grid:
return _buildGridView();
case ViewType.list:
return _buildListView();
}
}
Widget _buildListView() {
if (!hasLoaded && isLoading) {
return const CustomScrollView(slivers: [
DocumentsListLoadingWidget(),
]);
}
return ListView.builder(
controller: scrollController,
primary: false,
itemCount: documents.length,
itemBuilder: (context, index) {
final document = documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
),
);
},
);
}
Widget _buildGridView() {
if (!hasLoaded && isLoading) {
return const CustomScrollView(
slivers: [
DocumentsListLoadingWidget(),
],
); //TODO: Build grid skeleton
}
return GridView.builder(
controller: scrollController,
primary: false,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1 / 2,
),
itemCount: documents.length,
itemBuilder: (context, index) {
final document = documents.elementAt(index);
return DocumentGridItem(
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
isLabelClickable: isLabelClickable,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
);
},
);
}
}

View File

@@ -7,11 +7,11 @@ import 'package:paperless_mobile/generated/l10n.dart';
class DocumentsEmptyState extends StatelessWidget {
final PagedDocumentsState state;
final VoidCallback onReset;
final VoidCallback? onReset;
const DocumentsEmptyState({
Key? key,
required this.state,
required this.onReset,
this.onReset,
}) : super(key: key);
@override
@@ -20,7 +20,7 @@ class DocumentsEmptyState extends StatelessWidget {
child: EmptyState(
title: S.of(context).documentsPageEmptyStateOopsText,
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
bottomChild: state.filter != DocumentFilter.initial
bottomChild: state.filter != DocumentFilter.initial && onReset != null
? TextButton(
onPressed: onReset,
child: Text(

View File

@@ -0,0 +1,85 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:shimmer/shimmer.dart';
class DocumentsListLoadingWidget extends StatelessWidget {
static const _tags = [" ", " ", " "];
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
static const _correspondentLengths = <double>[200.0, 300.0, 150.0];
static const _fontSize = 16.0;
const DocumentsListLoadingWidget({super.key
});
@override
Widget build(BuildContext context) {
final _random = Random();
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return _buildFakeListItem(context, _random);
},
),
);
}
Widget _buildFakeListItem(BuildContext context, Random random) {
final tagCount = random.nextInt(_tags.length + 1);
final correspondentLength =
_correspondentLengths[random.nextInt(_correspondentLengths.length - 1)];
final titleLength = _titleLengths[random.nextInt(_titleLengths.length - 1)];
return Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[300]!
: Colors.grey[900]!,
highlightColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]!
: Colors.grey[600]!,
child: ListTile(
contentPadding: const EdgeInsets.all(8),
dense: true,
isThreeLine: true,
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.white,
height: 50,
width: 35,
),
),
title: Container(
padding: const EdgeInsets.symmetric(vertical: 2.0),
width: correspondentLength,
height: _fontSize,
color: Colors.white,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 2.0),
height: _fontSize,
width: titleLength,
color: Colors.white,
),
Wrap(
spacing: 2.0,
children: List.generate(
tagCount,
(index) => InputChip(
label: Text(_tags[random.nextInt(_tags.length)]),
),
),
).paddedOnly(top: 4),
],
),
),
),
);
}
}

View File

@@ -1,38 +1,35 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.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';
class DocumentGridItem extends StatelessWidget {
final DocumentModel document;
final bool isSelected;
final void Function(DocumentModel) onTap;
final void Function(DocumentModel) onSelected;
final bool isAtLeastOneSelected;
final bool Function(int tagId) isTagSelectedPredicate;
final void Function(int tagId)? onTagSelected;
class DocumentGridItem extends DocumentItem {
const DocumentGridItem({
Key? key,
required this.document,
required this.onTap,
required this.onSelected,
required this.isSelected,
required this.isAtLeastOneSelected,
required this.isTagSelectedPredicate,
required this.onTagSelected,
}) : super(key: key);
super.key,
required super.document,
required super.isSelected,
required super.isSelectionActive,
required super.isLabelClickable,
super.onCorrespondentSelected,
super.onDocumentTypeSelected,
super.onSelected,
super.onStoragePathSelected,
super.onTagSelected,
super.onTap,
required super.enableHeroAnimation,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onTap,
onLongPress: () => onSelected(document),
onLongPress: onSelected != null ? () => onSelected!(document) : null,
child: AbsorbPointer(
absorbing: isAtLeastOneSelected,
absorbing: isSelectionActive,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
@@ -48,6 +45,7 @@ class DocumentGridItem extends StatelessWidget {
child: DocumentPreview(
id: document.id,
borderRadius: 12.0,
enableHero: enableHeroAnimation,
),
),
Expanded(
@@ -94,10 +92,10 @@ class DocumentGridItem extends StatelessWidget {
}
void _onTap() {
if (isAtLeastOneSelected || isSelected) {
onSelected(document);
if (isSelectionActive || isSelected) {
onSelected?.call(document);
} else {
onTap(document);
onTap?.call(document);
}
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
abstract class DocumentItem extends StatelessWidget {
final DocumentModel document;
final void Function(DocumentModel)? onTap;
final void Function(DocumentModel)? onSelected;
final bool isSelected;
final bool isSelectionActive;
final bool isLabelClickable;
final bool enableHeroAnimation;
final void Function(int tagId)? onTagSelected;
final void Function(int? correspondentId)? onCorrespondentSelected;
final void Function(int? documentTypeId)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
const DocumentItem({
super.key,
required this.document,
this.onTap,
this.onSelected,
required this.isSelected,
required this.isSelectionActive,
required this.isLabelClickable,
this.onTagSelected,
this.onCorrespondentSelected,
this.onDocumentTypeSelected,
this.onStoragePathSelected,
required this.enableHeroAnimation,
});
}

View File

@@ -1,39 +1,26 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.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';
class DocumentListItem extends StatelessWidget {
class DocumentListItem extends DocumentItem {
static const _a4AspectRatio = 1 / 1.4142;
final DocumentModel document;
final void Function(DocumentModel)? onTap;
final void Function(DocumentModel)? onSelected;
final bool isSelected;
final bool isAtLeastOneSelected;
final bool isLabelClickable;
final void Function(int tagId)? onTagSelected;
final void Function(int? correspondentId)? onCorrespondentSelected;
final void Function(int? documentTypeId)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
final bool enableHeroAnimation;
const DocumentListItem({
Key? key,
required this.document,
this.onTap,
this.onSelected,
this.isSelected = false,
this.isAtLeastOneSelected = false,
this.isLabelClickable = true,
this.onTagSelected,
this.onCorrespondentSelected,
this.onDocumentTypeSelected,
this.onStoragePathSelected,
this.enableHeroAnimation = true,
}) : super(key: key);
super.key,
required super.document,
required super.isSelected,
required super.isSelectionActive,
required super.isLabelClickable,
super.onCorrespondentSelected,
super.onDocumentTypeSelected,
super.onSelected,
super.onStoragePathSelected,
super.onTagSelected,
super.onTap,
super.enableHeroAnimation = true,
});
@override
Widget build(BuildContext context) {
@@ -50,7 +37,7 @@ class DocumentListItem extends StatelessWidget {
Row(
children: [
AbsorbPointer(
absorbing: isAtLeastOneSelected,
absorbing: isSelectionActive,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondentId: document.correspondent,
@@ -69,7 +56,7 @@ class DocumentListItem extends StatelessWidget {
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: AbsorbPointer(
absorbing: isAtLeastOneSelected,
absorbing: isSelectionActive,
child: TagsWidget(
isClickable: isLabelClickable,
tagIds: document.tags,
@@ -95,7 +82,7 @@ class DocumentListItem extends StatelessWidget {
}
void _onTap() {
if (isAtLeastOneSelected || isSelected) {
if (isSelectionActive || isSelected) {
onSelected?.call(document);
} else {
onTap?.call(document);

View File

@@ -1,114 +0,0 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
class AdaptiveDocumentsView extends StatelessWidget {
final DocumentsState state;
final ViewType viewType;
final Widget? beforeItems;
final void Function(DocumentModel) onTap;
final void Function(DocumentModel) onSelected;
final ScrollController scrollController;
final bool hasInternetConnection;
final bool isLabelClickable;
final void Function(int id)? onTagSelected;
final void Function(int? id)? onCorrespondentSelected;
final void Function(int? id)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
final Widget pageLoadingWidget;
const AdaptiveDocumentsView({
super.key,
required this.onTap,
required this.scrollController,
required this.state,
required this.onSelected,
required this.hasInternetConnection,
this.isLabelClickable = true,
this.onTagSelected,
this.onCorrespondentSelected,
this.onDocumentTypeSelected,
this.onStoragePathSelected,
required this.pageLoadingWidget,
this.beforeItems,
required this.viewType,
});
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(child: beforeItems),
if (viewType == ViewType.list) _buildListView() else _buildGridView(),
if (state.hasLoaded && state.isLoading)
SliverToBoxAdapter(child: pageLoadingWidget),
],
);
}
SliverList _buildListView() {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: state.documents.length,
(context, index) {
final document = state.documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: state.selectedIds.contains(document.id),
onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
),
);
},
),
);
}
Widget _buildGridView() {
return SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1 / 2,
),
itemCount: state.documents.length,
itemBuilder: (context, index) {
if (state.hasLoaded &&
state.isLoading &&
index == state.documents.length) {
return Center(child: pageLoadingWidget);
}
final document = state.documents.elementAt(index);
return DocumentGridItem(
document: document,
onTap: onTap,
isSelected: state.selectedIds.contains(document.id),
onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
);
},
);
}
}

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);
}
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
class ViewActions extends StatelessWidget {
const ViewActions({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SortDocumentsButton(),
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
final cubit = context.read<ApplicationSettingsCubit>();
switch (settings.preferredViewType) {
case ViewType.grid:
return IconButton(
icon: const Icon(Icons.list),
onPressed: () =>
cubit.setViewType(settings.preferredViewType.toggle()),
);
case ViewType.list:
return IconButton(
icon: const Icon(Icons.grid_view_rounded),
onPressed: () =>
cubit.setViewType(settings.preferredViewType.toggle()),
);
}
},
)
],
);
}
}