Implemented m3 full screen bottom sheet for document search

This commit is contained in:
Anton Stubenbord
2023-01-02 23:37:53 +01:00
parent 97b69a7aae
commit 2445c97d44
4 changed files with 150 additions and 74 deletions

View File

@@ -11,6 +11,7 @@ class FormBuilderExtendedDateRangePicker extends StatefulWidget {
final String labelText; final String labelText;
final DateRangeQuery initialValue; final DateRangeQuery initialValue;
final void Function(DateRangeQuery? query)? onChanged; final void Function(DateRangeQuery? query)? onChanged;
const FormBuilderExtendedDateRangePicker({ const FormBuilderExtendedDateRangePicker({
super.key, super.key,
required this.name, required this.name,
@@ -65,7 +66,12 @@ class _FormBuilderExtendedDateRangePickerState
: null, : null,
), ),
), ),
RelativeDateRangePickerHelper(field: field), MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: RelativeDateRangePickerHelper(field: field),
),
], ],
); );
}, },

View File

@@ -23,7 +23,6 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:provider/provider.dart';
class DocumentsPage extends StatefulWidget { class DocumentsPage extends StatefulWidget {
const DocumentsPage({Key? key}) : super(key: key); const DocumentsPage({Key? key}) : super(key: key);
@@ -100,7 +99,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
} }
void _openDocumentFilter() async { void _openDocumentFilter() async {
final draggableSheetController = DraggableScrollableController();
final filter = await showModalBottomSheet<DocumentFilter>( final filter = await showModalBottomSheet<DocumentFilter>(
useSafeArea: true,
context: context, context: context,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@@ -112,14 +113,17 @@ class _DocumentsPageState extends State<DocumentsPage> {
builder: (_) => BlocProvider.value( builder: (_) => BlocProvider.value(
value: context.read<DocumentsCubit>(), value: context.read<DocumentsCubit>(),
child: DraggableScrollableSheet( child: DraggableScrollableSheet(
controller: draggableSheetController,
expand: false, expand: false,
snap: true, snap: true,
snapSizes: const [0.9, 1],
initialChildSize: .9, initialChildSize: .9,
snapSizes: const [.9, 1], maxChildSize: 1,
builder: (context, controller) => LabelsBlocProvider( builder: (context, controller) => LabelsBlocProvider(
child: DocumentFilterPanel( child: DocumentFilterPanel(
initialFilter: context.read<DocumentsCubit>().state.filter, initialFilter: context.read<DocumentsCubit>().state.filter,
scrollController: controller, scrollController: controller,
draggableSheetController: draggableSheetController,
), ),
), ),
), ),

View File

@@ -1,3 +1,7 @@
import 'dart:developer' as dev;
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
@@ -17,10 +21,12 @@ enum DateRangeSelection { before, after }
class DocumentFilterPanel extends StatefulWidget { class DocumentFilterPanel extends StatefulWidget {
final DocumentFilter initialFilter; final DocumentFilter initialFilter;
final ScrollController scrollController; final ScrollController scrollController;
final DraggableScrollableController draggableSheetController;
const DocumentFilterPanel({ const DocumentFilterPanel({
Key? key, Key? key,
required this.initialFilter, required this.initialFilter,
required this.scrollController, required this.scrollController,
required this.draggableSheetController,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -38,21 +44,49 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
final _formKey = GlobalKey<FormBuilderState>(); final _formKey = GlobalKey<FormBuilderState>();
late bool _allowOnlyExtendedQuery; late bool _allowOnlyExtendedQuery;
double _heightAnimationValue = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery; _allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery;
widget.draggableSheetController.addListener(animateTitleByDrag);
}
void animateTitleByDrag() {
setState(
() {
_heightAnimationValue = dp(
((max(0.9, widget.draggableSheetController.size) - 0.9) / 0.1), 5);
},
);
}
bool get isDockedToTop => _heightAnimationValue == 1;
@override
void dispose() {
widget.draggableSheetController.removeListener(animateTitleByDrag);
super.dispose();
}
/// Rounds double to [places] decimal places.
double dp(double val, int places) {
num mod = pow(10.0, places);
return ((val * mod).round().toDouble() / mod);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double radius = (1 - max(0, (_heightAnimationValue) - 0.5) * 2) * 16;
return ClipRRect( return ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(radius),
topRight: Radius.circular(16), topRight: Radius.circular(radius),
), ),
child: Scaffold( child: Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
backgroundColor: Theme.of(context).colorScheme.surface,
floatingActionButton: Visibility( floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0, visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended( child: FloatingActionButton.extended(
@@ -69,7 +103,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
onPressed: _resetFilter, onPressed: _resetFilter,
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: Text(S.of(context).documentFilterResetLabel), label: Text(S.of(context).documentFilterResetLabel),
) ),
], ],
), ),
), ),
@@ -82,67 +116,97 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
ListView _buildFormList(BuildContext context) { Widget _buildFormList(BuildContext context) {
return ListView( return CustomScrollView(
controller: widget.scrollController, controller: widget.scrollController,
children: [ slivers: [
Align( SliverAppBar(
alignment: Alignment.center, pinned: true,
child: _buildDragHandle(context), automaticallyImplyLeading: false,
), toolbarHeight: kToolbarHeight + 22,
Text( title: SizedBox(
S.of(context).documentFilterTitle, width: MediaQuery.of(context).size.width,
style: Theme.of(context).textTheme.headlineSmall, child: Column(
).paddedOnly( mainAxisSize: MainAxisSize.max,
top: 16.0, crossAxisAlignment: CrossAxisAlignment.center,
left: 16.0, children: [
bottom: 24, Opacity(
), opacity: 1 - _heightAnimationValue,
Align( child: Padding(
alignment: Alignment.centerLeft, padding: EdgeInsets.only(bottom: 11),
child: Text( child: _buildDragHandle(),
S.of(context).documentFilterSearchLabel, ),
style: Theme.of(context).textTheme.bodySmall, ),
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),
),
],
),
),
],
),
), ),
).paddedOnly(left: 8.0), ),
_buildQueryFormField().padded(), ..._buildFormFieldList(),
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentFilterAdvancedLabel,
style: Theme.of(context).textTheme.bodySmall,
),
).padded(),
FormBuilderExtendedDateRangePicker(
name: fkCreatedAt,
initialValue: widget.initialFilter.created,
labelText: S.of(context).documentCreatedPropertyLabel,
onChanged: (_) {
_checkQueryConstraints();
},
).padded(),
FormBuilderExtendedDateRangePicker(
name: fkAddedAt,
initialValue: widget.initialFilter.added,
labelText: S.of(context).documentAddedPropertyLabel,
onChanged: (_) {
_checkQueryConstraints();
},
).padded(),
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
_buildTagsFormField().padded(),
], ],
); );
} }
Container _buildDragHandle(BuildContext context) { 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( return Container(
// According to m3 spec // According to m3 spec https://m3.material.io/components/bottom-sheets/specs
width: 32, width: 32,
height: 4, height: 4,
margin: const EdgeInsets.only(top: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4), color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),

View File

@@ -28,24 +28,26 @@ class TextQueryFormField extends StatelessWidget {
prefixIcon: const Icon(Icons.search_outlined), prefixIcon: const Icon(Icons.search_outlined),
labelText: _buildLabelText(context, field.value!.queryType), labelText: _buildLabelText(context, field.value!.queryType),
suffixIcon: PopupMenuButton<QueryType>( suffixIcon: PopupMenuButton<QueryType>(
enabled: !onlyExtendedQueryAllowed,
color: onlyExtendedQueryAllowed
? Theme.of(context).disabledColor
: null,
itemBuilder: (context) => [ itemBuilder: (context) => [
if (!onlyExtendedQueryAllowed) ...[ PopupMenuItem(
PopupMenuItem( child: ListTile(
child: ListTile( title: Text(S
title: Text(S .of(context)
.of(context) .documentFilterQueryOptionsTitleAndContentLabel),
.documentFilterQueryOptionsTitleAndContentLabel),
),
value: QueryType.titleAndContent,
), ),
PopupMenuItem( value: QueryType.titleAndContent,
child: ListTile( ),
title: Text( PopupMenuItem(
S.of(context).documentFilterQueryOptionsTitleLabel), child: ListTile(
), title: Text(
value: QueryType.title, S.of(context).documentFilterQueryOptionsTitleLabel),
), ),
], value: QueryType.title,
),
PopupMenuItem( PopupMenuItem(
child: ListTile( child: ListTile(
title: Text( title: Text(