mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 16:07:57 -06:00
WIP - Refactoring date range picker dialog
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_relative_date_range_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
|
||||
class ExtendedDateRangeDialog extends StatefulWidget {
|
||||
final DateRangeQuery initialValue;
|
||||
final String Function(DateRangeQuery query) stringTransformer;
|
||||
const ExtendedDateRangeDialog({
|
||||
super.key,
|
||||
required this.initialValue,
|
||||
required this.stringTransformer,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExtendedDateRangeDialog> createState() =>
|
||||
_ExtendedDateRangeDialogState();
|
||||
}
|
||||
|
||||
class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
|
||||
static const String _fkAbsoluteBefore = 'absoluteBefore';
|
||||
static const String _fkAbsoluteAfter = 'absoluteAfter';
|
||||
static const String _fkRelative = 'relative';
|
||||
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
late DateRangeType _selectedDateRangeType;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDateRangeType = (widget.initialValue is RelativeDateRangeQuery)
|
||||
? DateRangeType.relative
|
||||
: DateRangeType.absolute;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text("Select date range"),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Hint: You can either specify absolute values by selecting concrete dates, or you can specify a time range relative to today.",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
_buildDateRangeQueryTypeSelection(),
|
||||
const SizedBox(height: 16),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
switch (_selectedDateRangeType) {
|
||||
case DateRangeType.absolute:
|
||||
return _buildAbsoluteDateRangeForm();
|
||||
case DateRangeType.relative:
|
||||
return FormBuilderRelativeDateRangePicker(
|
||||
initialValue:
|
||||
widget.initialValue is RelativeDateRangeQuery
|
||||
? widget.initialValue as RelativeDateRangeQuery
|
||||
: const RelativeDateRangeQuery(
|
||||
1,
|
||||
DateRangeUnit.month,
|
||||
),
|
||||
name: _fkRelative,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(S.of(context).genericActionCancelLabel),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
TextButton(
|
||||
child: Text(S.of(context).genericActionSaveLabel),
|
||||
onPressed: () {
|
||||
_formKey.currentState?.save();
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
final values = _formKey.currentState!.value;
|
||||
final query = _buildQuery(values);
|
||||
Navigator.pop(context, query);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateRangeQueryTypeSelection() {
|
||||
return Row(
|
||||
children: [
|
||||
ChoiceChip(
|
||||
label: Text('Absolute'),
|
||||
selected: _selectedDateRangeType == DateRangeType.absolute,
|
||||
onSelected: (value) =>
|
||||
setState(() => _selectedDateRangeType = DateRangeType.absolute),
|
||||
).paddedOnly(right: 8.0),
|
||||
ChoiceChip(
|
||||
label: Text('Relative'),
|
||||
selected: _selectedDateRangeType == DateRangeType.relative,
|
||||
onSelected: (value) =>
|
||||
setState(() => _selectedDateRangeType = DateRangeType.relative),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAbsoluteDateRangeForm() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
FormBuilderDateTimePicker(
|
||||
name: _fkAbsoluteAfter,
|
||||
initialDate: widget.initialValue is AbsoluteDateRangeQuery
|
||||
? (widget.initialValue as AbsoluteDateRangeQuery).after
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).extendedDateRangePickerAfterLabel,
|
||||
),
|
||||
inputType: InputType.date,
|
||||
),
|
||||
FormBuilderDateTimePicker(
|
||||
name: _fkAbsoluteBefore,
|
||||
initialDate: widget.initialValue is AbsoluteDateRangeQuery
|
||||
? (widget.initialValue as AbsoluteDateRangeQuery).before
|
||||
: null,
|
||||
inputType: InputType.date,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).extendedDateRangePickerBeforeLabel,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
DateRangeQuery? _buildQuery(Map<String, dynamic> values) {
|
||||
if (_selectedDateRangeType == DateRangeType.absolute) {
|
||||
return AbsoluteDateRangeQuery(
|
||||
after: values[_fkAbsoluteAfter],
|
||||
before: values[_fkAbsoluteBefore],
|
||||
);
|
||||
} else {
|
||||
return values[_fkRelative] as RelativeDateRangeQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DateRangeType {
|
||||
absolute,
|
||||
relative;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_dialog.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class FormBuilderExtendedDateRangePicker extends StatefulWidget {
|
||||
final String name;
|
||||
final String labelText;
|
||||
final DateRangeQuery initialValue;
|
||||
|
||||
const FormBuilderExtendedDateRangePicker({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.labelText,
|
||||
required this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FormBuilderExtendedDateRangePicker> createState() =>
|
||||
_FormBuilderExtendedDateRangePickerState();
|
||||
}
|
||||
|
||||
class _FormBuilderExtendedDateRangePickerState
|
||||
extends State<FormBuilderExtendedDateRangePicker> {
|
||||
late final TextEditingController _textEditingController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController = TextEditingController(
|
||||
text: _dateRangeQueryToString(widget.initialValue));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderField<DateRangeQuery>(
|
||||
name: widget.name,
|
||||
initialValue: widget.initialValue,
|
||||
onChanged: (query) {
|
||||
_textEditingController.text =
|
||||
_dateRangeQueryToString(query ?? const UnsetDateRangeQuery());
|
||||
},
|
||||
builder: (field) {
|
||||
return Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _textEditingController,
|
||||
readOnly: true,
|
||||
onTap: () => _showExtendedDateRangePicker(field),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.date_range),
|
||||
labelText: widget.labelText,
|
||||
),
|
||||
),
|
||||
_buildExtendedQueryOptions(field),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExtendedQueryOptions(FormFieldState<DateRangeQuery> field) {
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: ListView.separated(
|
||||
itemCount: _options.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8.0),
|
||||
itemBuilder: (context, index) {
|
||||
final option = _options[index];
|
||||
return FilterChip(
|
||||
label: Text(option.title),
|
||||
onSelected: (isSelected) => isSelected
|
||||
? field.didChange(option.value)
|
||||
: field.didChange(const UnsetDateRangeQuery()),
|
||||
selected: field.value == option.value,
|
||||
);
|
||||
},
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<_ExtendedDateRangeQueryOption> get _options => [
|
||||
_ExtendedDateRangeQueryOption(
|
||||
S.of(context).extendedDateRangePickerLastWeeksLabel(1),
|
||||
const RelativeDateRangeQuery(1, DateRangeUnit.week),
|
||||
),
|
||||
_ExtendedDateRangeQueryOption(
|
||||
S.of(context).extendedDateRangePickerLastMonthsLabel(1),
|
||||
const RelativeDateRangeQuery(1, DateRangeUnit.month),
|
||||
),
|
||||
_ExtendedDateRangeQueryOption(
|
||||
S.of(context).extendedDateRangePickerLastMonthsLabel(3),
|
||||
const RelativeDateRangeQuery(3, DateRangeUnit.month),
|
||||
),
|
||||
_ExtendedDateRangeQueryOption(
|
||||
S.of(context).extendedDateRangePickerLastYearsLabel(1),
|
||||
const RelativeDateRangeQuery(1, DateRangeUnit.year),
|
||||
),
|
||||
];
|
||||
|
||||
String _dateRangeQueryToString(DateRangeQuery query) {
|
||||
if (query is UnsetDateRangeQuery) {
|
||||
return '';
|
||||
} else if (query is AbsoluteDateRangeQuery) {
|
||||
if (query.before != null && query.after != null) {
|
||||
return '${DateFormat.yMd(query.after)} – ${DateFormat.yMd(query.before)}';
|
||||
}
|
||||
if (query.before != null) {
|
||||
return '${S.of(context).extendedDateRangePickerBeforeLabel} ${DateFormat.yMd(query.before)}';
|
||||
}
|
||||
if (query.after != null) {
|
||||
return '${S.of(context).extendedDateRangePickerAfterLabel} ${DateFormat.yMd(query.after)}';
|
||||
}
|
||||
} else if (query is RelativeDateRangeQuery) {
|
||||
switch (query.unit) {
|
||||
case DateRangeUnit.day:
|
||||
return S
|
||||
.of(context)
|
||||
.extendedDateRangePickerLastDaysLabel(query.offset);
|
||||
case DateRangeUnit.week:
|
||||
return S
|
||||
.of(context)
|
||||
.extendedDateRangePickerLastWeeksLabel(query.offset);
|
||||
case DateRangeUnit.month:
|
||||
return S
|
||||
.of(context)
|
||||
.extendedDateRangePickerLastMonthsLabel(query.offset);
|
||||
case DateRangeUnit.year:
|
||||
return S
|
||||
.of(context)
|
||||
.extendedDateRangePickerLastYearsLabel(query.offset);
|
||||
default:
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
void _showExtendedDateRangePicker(
|
||||
FormFieldState<DateRangeQuery> field,
|
||||
) async {
|
||||
final query = await showDialog<DateRangeQuery>(
|
||||
context: context,
|
||||
builder: (context) => ExtendedDateRangeDialog(
|
||||
initialValue: field.value!,
|
||||
stringTransformer: _dateRangeQueryToString,
|
||||
),
|
||||
);
|
||||
if (query != null) {
|
||||
field.didChange(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtendedDateRangeQueryOption {
|
||||
final String title;
|
||||
final RelativeDateRangeQuery value;
|
||||
|
||||
_ExtendedDateRangeQueryOption(this.title, this.value);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class FormBuilderRelativeDateRangePicker extends StatefulWidget {
|
||||
final String name;
|
||||
final RelativeDateRangeQuery initialValue;
|
||||
const FormBuilderRelativeDateRangePicker({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FormBuilderRelativeDateRangePicker> createState() =>
|
||||
_FormBuilderRelativeDateRangePickerState();
|
||||
}
|
||||
|
||||
class _FormBuilderRelativeDateRangePickerState
|
||||
extends State<FormBuilderRelativeDateRangePicker> {
|
||||
late int _offset;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_offset = widget.initialValue.offset;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderField<RelativeDateRangeQuery>(
|
||||
name: widget.name,
|
||||
initialValue: widget.initialValue,
|
||||
builder: (field) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Last"),
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Offset",
|
||||
),
|
||||
initialValue: widget.initialValue.offset.toString(),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r"[1-9][0-9]*"))
|
||||
],
|
||||
validator: FormBuilderValidators.numeric(),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final parsed = int.parse(value);
|
||||
setState(() {
|
||||
_offset = parsed;
|
||||
});
|
||||
field.didChange(field.value!.copyWith(offset: parsed));
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: DropdownButtonFormField<DateRangeUnit?>(
|
||||
value: field.value?.unit,
|
||||
items: DateRangeUnit.values
|
||||
.map(
|
||||
(unit) => DropdownMenuItem(
|
||||
child: Text(
|
||||
_dateRangeUnitToLocalizedString(
|
||||
unit,
|
||||
_offset,
|
||||
),
|
||||
),
|
||||
value: unit,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) =>
|
||||
field.didChange(field.value!.copyWith(unit: value)),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Amount",
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _dateRangeUnitToLocalizedString(DateRangeUnit unit, int? count) {
|
||||
switch (unit) {
|
||||
case DateRangeUnit.day:
|
||||
return S.of(context).extendedDateRangePickerDayText(count ?? 1);
|
||||
case DateRangeUnit.week:
|
||||
return S.of(context).extendedDateRangePickerWeekText(count ?? 1);
|
||||
case DateRangeUnit.month:
|
||||
return S.of(context).extendedDateRangePickerMonthText(count ?? 1);
|
||||
case DateRangeUnit.year:
|
||||
return S.of(context).extendedDateRangePickerYearText(count ?? 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user