mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 12:07:58 -06:00
Implemented m3 full screen bottom sheet for document search
This commit is contained in:
@@ -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),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user