Bugfixes, finished filter rework

This commit is contained in:
Anton Stubenbord
2022-12-14 00:53:42 +01:00
parent 3dc590baa2
commit f001059401
20 changed files with 566 additions and 804 deletions

View File

@@ -66,55 +66,66 @@ class _DocumentsPageState extends State<DocumentsPage> {
previous != ConnectivityState.connected &&
current == ConnectivityState.connected,
listener: (context, state) {
_documentsCubit.load();
try {
_documentsCubit.load();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
builder: (context, connectivityState) {
return Scaffold(
drawer: BlocProvider.value(
value: BlocProvider.of<AuthenticationCubit>(context),
child: InfoDrawer(
afterInboxClosed: () => _documentsCubit.reload(),
),
drawer: BlocProvider.value(
value: BlocProvider.of<AuthenticationCubit>(context),
child: InfoDrawer(
afterInboxClosed: () => _documentsCubit.reload(),
),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount;
return Badge(
toAnimate: false,
showBadge: appliedFiltersCount > 0,
badgeContent: appliedFiltersCount > 0
? Text(state.filter.appliedFiltersCount.toString())
: null,
child: FloatingActionButton(
child: const Icon(Icons.filter_alt),
onPressed: _openDocumentFilter,
),
);
},
),
resizeToAvoidBottomInset: true,
body: _buildBody(connectivityState));
),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount;
return Badge(
toAnimate: false,
animationType: BadgeAnimationType.fade,
showBadge: appliedFiltersCount > 0,
badgeContent: appliedFiltersCount > 0
? Text(
state.filter.appliedFiltersCount.toString(),
style: const TextStyle(color: Colors.white),
)
: null,
child: FloatingActionButton(
child: const Icon(Icons.filter_alt_rounded),
onPressed: _openDocumentFilter,
),
);
},
),
resizeToAvoidBottomInset: true,
body: _buildBody(connectivityState),
);
},
);
}
void _openDocumentFilter() async {
final filter = await showModalBottomSheet(
final filter = await showModalBottomSheet<DocumentFilter>(
context: context,
builder: (context) => SizedBox(
height: MediaQuery.of(context).size.height - kToolbarHeight - 16,
child: LabelsBlocProvider(
child: DocumentFilterPanel(
initialFilter: _documentsCubit.state.filter,
),
),
),
isDismissible: true,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: .9,
builder: (context, controller) => LabelsBlocProvider(
child: DocumentFilterPanel(
initialFilter: _documentsCubit.state.filter,
scrollController: controller,
),
),
),
);
@@ -125,6 +136,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
Widget _buildBody(ConnectivityState connectivityState) {
final isConnected = connectivityState == ConnectivityState.connected;
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
return BlocBuilder<DocumentsCubit, DocumentsState>(
@@ -143,8 +155,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
state: state,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection:
connectivityState == ConnectivityState.connected,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
);
break;
@@ -154,8 +165,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
state: state,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection:
connectivityState == ConnectivityState.connected,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
);
break;
@@ -175,6 +185,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
return RefreshIndicator(
onRefresh: _onRefresh,
notificationPredicate: (_) => isConnected,
child: CustomScrollView(
slivers: [
BlocListener<SavedViewCubit, SavedViewState>(
@@ -198,6 +209,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
},
child: DocumentsPageAppBar(
isOffline:
connectivityState != ConnectivityState.connected,
actions: [
const SortDocumentsButton(),
IconButton(

View File

@@ -41,6 +41,7 @@ class DocumentGridItem extends StatelessWidget {
? Theme.of(context).colorScheme.inversePrimary
: Theme.of(context).cardColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1,
@@ -74,8 +75,9 @@ class DocumentGridItem extends StatelessWidget {
),
const Spacer(),
Text(
DateFormat.yMMMd(Intl.getCurrentLocale())
.format(document.created),
DateFormat.yMMMd().format(
document.created,
),
style: Theme.of(context).textTheme.caption,
),
],

View File

@@ -16,10 +16,11 @@ enum DateRangeSelection { before, after }
class DocumentFilterPanel extends StatefulWidget {
final DocumentFilter initialFilter;
final ScrollController scrollController;
const DocumentFilterPanel({
Key? key,
required this.initialFilter,
required this.scrollController,
}) : super(key: key);
@override
@@ -36,80 +37,68 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
final _formKey = GlobalKey<FormBuilderState>();
DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) {
if (start == null && end == null) {
return null;
}
if (start != null && end != null) {
return DateTimeRange(start: start, end: end);
}
assert(start != null || end != null);
final singleDate = (start ?? end)!;
return DateTimeRange(start: singleDate, end: singleDate);
}
@override
Widget build(BuildContext context) {
const radius = Radius.circular(16);
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: radius,
topRight: radius,
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
child: Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended(
icon: const Icon(Icons.done),
label: Text(S.of(context).documentFilterApplyFilterLabel),
onPressed: _onApplyFilter,
),
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextButton.icon(
onPressed: _resetFilter,
icon: const Icon(Icons.refresh),
label: Text(S.of(context).documentFilterResetLabel),
)
],
),
),
resizeToAvoidBottomInset: true,
body: FormBuilder(
key: _formKey,
child: Column(
child: ListView(
controller: widget.scrollController,
children: [
_buildDraggableResetHeader(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).documentFilterTitle,
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: _onApplyFilter,
child: Text(S.of(context).documentFilterApplyFilterLabel),
),
],
).padded(),
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
child: SingleChildScrollView(
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(S.of(context).documentFilterSearchLabel),
).paddedOnly(left: 8.0),
_buildQueryFormField().padded(),
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentFilterAdvancedLabel,
),
).padded(),
_buildCreatedDateRangePickerFormField(),
_buildAddedDateRangePickerFormField(),
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
_buildTagsFormField()
.paddedSymmetrically(horizontal: 8, vertical: 4.0),
],
).paddedOnly(bottom: 16),
),
),
Text(
S.of(context).documentFilterTitle,
style: Theme.of(context).textTheme.headlineSmall,
).paddedOnly(
top: 16.0,
left: 16.0,
bottom: 24,
),
Align(
alignment: Alignment.centerLeft,
child: Text(S.of(context).documentFilterSearchLabel),
).paddedOnly(left: 8.0),
_buildQueryFormField().padded(),
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentFilterAdvancedLabel,
),
).padded(),
_buildCreatedDateRangePickerFormField(),
_buildAddedDateRangePickerFormField(),
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
_buildTagsFormField().padded(),
],
),
).paddedOnly(bottom: 16),
),
),
);
@@ -128,29 +117,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Stack _buildDraggableResetHeader() {
return Stack(
alignment: Alignment.center,
children: [
_buildDragLine(),
Align(
alignment: Alignment.topRight,
child: TextButton.icon(
icon: const Icon(Icons.refresh),
label: Text(S.of(context).documentFilterResetLabel),
onPressed: () => _resetFilter(context),
),
),
],
);
}
void _resetFilter(BuildContext context) async {
void _resetFilter() async {
FocusScope.of(context).unfocus();
Navigator.pop(context, DocumentFilter.initial);
}
//TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField!
Widget _buildDocumentTypeFormField() {
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
@@ -416,42 +387,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildDragLine() {
return Container(
width: 48,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
),
);
}
void _onApplyFilter() async {
_formKey.currentState?.save();
if (_formKey.currentState?.validate() ?? false) {
final v = _formKey.currentState!.value;
DocumentFilter newFilter = DocumentFilter(
createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end,
createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start,
correspondent: v[fkCorrespondent] as CorrespondentQuery? ??
DocumentFilter.initial.correspondent,
documentType: v[fkDocumentType] as DocumentTypeQuery? ??
DocumentFilter.initial.documentType,
storagePath: v[fkStoragePath] as StoragePathQuery? ??
DocumentFilter.initial.storagePath,
tags: v[DocumentModel.tagsKey] as TagsQuery? ??
DocumentFilter.initial.tags,
queryText: v[fkQuery] as String?,
addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end,
addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
asnQuery: widget.initialFilter.asnQuery,
page: 1,
pageSize: widget.initialFilter.pageSize,
sortField: widget.initialFilter.sortField,
sortOrder: widget.initialFilter.sortOrder,
);
DocumentFilter newFilter = _assembleFilter();
try {
FocusScope.of(context).unfocus();
Navigator.pop(context, newFilter);
@@ -461,23 +401,40 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
}
}
void _patchFromFilter(DocumentFilter f) {
_formKey.currentState?.patchValue({
fkCorrespondent: f.correspondent,
fkDocumentType: f.documentType,
fkQuery: f.queryText,
fkStoragePath: f.storagePath,
DocumentModel.tagsKey: f.tags,
DocumentModel.titleKey: f.queryText,
QueryTypeFormField.fkQueryType: f.queryType,
fkCreatedAt: _dateTimeRangeOfNullable(
f.createdDateAfter,
f.createdDateBefore,
),
fkAddedAt: _dateTimeRangeOfNullable(
f.addedDateAfter,
f.addedDateBefore,
),
});
DocumentFilter _assembleFilter() {
final v = _formKey.currentState!.value;
return DocumentFilter(
createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end,
createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start,
correspondent: v[fkCorrespondent] as CorrespondentQuery? ??
DocumentFilter.initial.correspondent,
documentType: v[fkDocumentType] as DocumentTypeQuery? ??
DocumentFilter.initial.documentType,
storagePath: v[fkStoragePath] as StoragePathQuery? ??
DocumentFilter.initial.storagePath,
tags:
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
queryText: v[fkQuery] as String?,
addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end,
addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
asnQuery: widget.initialFilter.asnQuery,
page: 1,
pageSize: widget.initialFilter.pageSize,
sortField: widget.initialFilter.sortField,
sortOrder: widget.initialFilter.sortOrder,
);
}
}
DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) {
if (start == null && end == null) {
return null;
}
if (start != null && end != null) {
return DateTimeRange(start: start, end: end);
}
assert(start != null || end != null);
final singleDate = (start ?? end)!;
return DateTimeRange(start: singleDate, end: singleDate);
}

View File

@@ -1,17 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget {
final List<Widget> actions;
final bool isOffline;
const DocumentsPageAppBar({
super.key,
required this.isOffline,
this.actions = const [],
});
@override
@@ -21,19 +25,27 @@ class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget {
}
class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
static const _flexibleAreaHeight = kToolbarHeight + 48.0;
@override
Widget build(BuildContext context) {
const savedViewWidgetHeight = 48.0;
final flexibleAreaHeight = kToolbarHeight -
16 +
savedViewWidgetHeight +
(widget.isOffline ? 24 : 0);
return BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, documentsState) {
final hasSelection = documentsState.selection.isNotEmpty;
if (hasSelection) {
return SliverAppBar(
expandedHeight: kToolbarHeight + _flexibleAreaHeight,
expandedHeight: kToolbarHeight + flexibleAreaHeight,
snap: true,
floating: true,
pinned: true,
flexibleSpace: _buildFlexibleArea(false, documentsState.filter),
flexibleSpace: _buildFlexibleArea(
false,
documentsState.filter,
savedViewWidgetHeight,
),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () =>
@@ -50,13 +62,14 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
);
} else {
return SliverAppBar(
expandedHeight: kToolbarHeight + _flexibleAreaHeight,
expandedHeight: kToolbarHeight + flexibleAreaHeight,
snap: true,
floating: true,
pinned: true,
flexibleSpace: _buildFlexibleArea(
true,
documentsState.filter,
savedViewWidgetHeight,
),
title: Text(
'${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})',
@@ -70,30 +83,31 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
);
}
Widget _buildFlexibleArea(bool enabled, DocumentFilter filter) {
Widget _buildFlexibleArea(
bool enabled,
DocumentFilter filter,
double savedViewHeight,
) {
return FlexibleSpaceBar(
background: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SavedViewSelectionWidget(
height: 48,
enabled: enabled,
currentFilter: filter,
),
],
),
background: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (widget.isOffline) const OfflineBanner(),
SavedViewSelectionWidget(
height: savedViewHeight,
enabled: enabled,
currentFilter: filter,
).paddedSymmetrically(horizontal: 8.0),
],
),
);
}
void _onDelete(BuildContext context, DocumentsState documentsState) async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) =>
BulkDeleteConfirmationDialog(state: documentsState),
) ??
context: context,
builder: (context) =>
BulkDeleteConfirmationDialog(state: documentsState)) ??
false;
if (shouldDelete) {
try {