First working version of new date range picker

This commit is contained in:
Anton Stubenbord
2022-12-19 23:15:02 +01:00
parent 901d646ec2
commit a4a7593fb1
16 changed files with 1473 additions and 588 deletions

View File

@@ -1,19 +1,22 @@
import 'dart:developer';
import 'dart:math';
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:intl/intl.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/core/widgets/form_builder_fields/extended_date_range_form_field/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
@@ -26,12 +29,21 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
static const String _fkAbsoluteAfter = 'absoluteAfter';
static const String _fkRelative = 'relative';
DateTime? _before;
DateTime? _after;
final _formKey = GlobalKey<FormBuilderState>();
late DateRangeType _selectedDateRangeType;
@override
void initState() {
super.initState();
_selectedDateRangeType = (widget.initialValue is RelativeDateRangeQuery)
final initialQuery = widget.initialValue;
if (initialQuery is AbsoluteDateRangeQuery) {
_before = initialQuery.before;
_after = initialQuery.after;
}
_selectedDateRangeType = (initialQuery is RelativeDateRangeQuery)
? DateRangeType.relative
: DateRangeType.absolute;
}
@@ -40,13 +52,13 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Select date range"),
content: Form(
content: FormBuilder(
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.",
"Hint: You can either specify absolute values by selecting concrete dates, or you can specify a time range relative to the current date.",
style: Theme.of(context).textTheme.caption,
),
_buildDateRangeQueryTypeSelection(),
@@ -58,6 +70,7 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
return _buildAbsoluteDateRangeForm();
case DateRangeType.relative:
return FormBuilderRelativeDateRangePicker(
name: _fkRelative,
initialValue:
widget.initialValue is RelativeDateRangeQuery
? widget.initialValue as RelativeDateRangeQuery
@@ -65,7 +78,6 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
1,
DateRangeUnit.month,
),
name: _fkRelative,
);
}
},
@@ -118,23 +130,58 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
children: [
FormBuilderDateTimePicker(
name: _fkAbsoluteAfter,
initialDate: widget.initialValue is AbsoluteDateRangeQuery
initialValue: widget.initialValue is AbsoluteDateRangeQuery
? (widget.initialValue as AbsoluteDateRangeQuery).after
: null,
initialDate: _before?.subtract(const Duration(days: 1)),
decoration: InputDecoration(
labelText: S.of(context).extendedDateRangePickerAfterLabel,
prefixIcon: const Icon(Icons.date_range),
suffixIcon: _after != null
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_formKey.currentState?.fields[_fkAbsoluteAfter]
?.didChange(null);
setState(() => _after = null);
},
)
: null,
),
format: DateFormat.yMd(),
lastDate: _dateTimeMax(_before, DateTime.now()),
inputType: InputType.date,
onChanged: (after) {
setState(() => _after = after);
},
),
const SizedBox(height: 16),
FormBuilderDateTimePicker(
name: _fkAbsoluteBefore,
initialDate: widget.initialValue is AbsoluteDateRangeQuery
initialValue: widget.initialValue is AbsoluteDateRangeQuery
? (widget.initialValue as AbsoluteDateRangeQuery).before
: null,
inputType: InputType.date,
decoration: InputDecoration(
labelText: S.of(context).extendedDateRangePickerBeforeLabel,
prefixIcon: const Icon(Icons.date_range),
suffixIcon: _before != null
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_formKey.currentState?.fields[_fkAbsoluteBefore]
?.didChange(null);
setState(() => _before = null);
},
)
: null,
),
format: DateFormat.yMd(),
firstDate: _after,
lastDate: DateTime.now(),
onChanged: (before) {
setState(() => _before = before);
},
),
],
);
@@ -150,6 +197,12 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
return values[_fkRelative] as RelativeDateRangeQuery;
}
}
DateTime? _dateTimeMax(DateTime? dt1, DateTime? dt2) {
if (dt1 == null) return dt2;
if (dt2 == null) return dt1;
return dt1.compareTo(dt2) >= 0 ? dt1 : dt2;
}
}
enum DateRangeType {

View File

@@ -2,7 +2,8 @@ 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/core/widgets/form_builder_fields/extended_date_range_form_field/extended_date_range_dialog.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/relative_date_range_picker_helper.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class FormBuilderExtendedDateRangePicker extends StatefulWidget {
@@ -24,13 +25,13 @@ class FormBuilderExtendedDateRangePicker extends StatefulWidget {
class _FormBuilderExtendedDateRangePickerState
extends State<FormBuilderExtendedDateRangePicker> {
late final TextEditingController _textEditingController;
final TextEditingController _textEditingController = TextEditingController();
@override
void initState() {
super.initState();
_textEditingController = TextEditingController(
text: _dateRangeQueryToString(widget.initialValue));
void didChangeDependencies() {
super.didChangeDependencies();
// This has to be initialized here and not in initState because it has to be waited until dependencies for localization have been loaded.
_textEditingController.text = _dateRangeQueryToString(widget.initialValue);
}
@override
@@ -52,67 +53,39 @@ class _FormBuilderExtendedDateRangePickerState
decoration: InputDecoration(
prefixIcon: const Icon(Icons.date_range),
labelText: widget.labelText,
suffixIcon: _textEditingController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
field.didChange(const UnsetDateRangeQuery());
},
)
: null,
),
),
_buildExtendedQueryOptions(field),
RelativeDateRangePickerHelper(field: 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) {
final df = DateFormat.yMd();
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!.isAtSameMomentAs(query.after!)) {
return df.format(query.before!);
}
return '${df.format(query.after!)} ${df.format(query.before!)}';
}
if (query.before != null) {
return '${S.of(context).extendedDateRangePickerBeforeLabel} ${DateFormat.yMd(query.before)}';
return '${S.of(context).extendedDateRangePickerBeforeLabel} ${df.format(query.before!)}';
}
if (query.after != null) {
return '${S.of(context).extendedDateRangePickerAfterLabel} ${DateFormat.yMd(query.after)}';
return '${S.of(context).extendedDateRangePickerAfterLabel} ${df.format(query.after!)}';
}
} else if (query is RelativeDateRangeQuery) {
switch (query.unit) {
@@ -143,20 +116,10 @@ class _FormBuilderExtendedDateRangePickerState
) async {
final query = await showDialog<DateRangeQuery>(
context: context,
builder: (context) => ExtendedDateRangeDialog(
initialValue: field.value!,
stringTransformer: _dateRangeQueryToString,
),
builder: (context) => ExtendedDateRangeDialog(initialValue: field.value!),
);
if (query != null) {
field.didChange(query);
}
}
}
class _ExtendedDateRangeQueryOption {
final String title;
final RelativeDateRangeQuery value;
_ExtendedDateRangeQueryOption(this.title, this.value);
}

View File

@@ -0,0 +1,126 @@
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/extended_date_range_form_field/relative_date_range_picker_helper.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class FormBuilderRelativeDateRangePicker extends StatefulWidget {
final String name;
final RelativeDateRangeQuery initialValue;
final void Function(RelativeDateRangeQuery? query)? onChanged;
const FormBuilderRelativeDateRangePicker({
super.key,
required this.name,
required this.initialValue,
this.onChanged,
});
@override
State<FormBuilderRelativeDateRangePicker> createState() =>
_FormBuilderRelativeDateRangePickerState();
}
class _FormBuilderRelativeDateRangePickerState
extends State<FormBuilderRelativeDateRangePicker> {
late int _offset;
late final TextEditingController _offsetTextEditingController;
@override
void initState() {
super.initState();
_offset = widget.initialValue.offset;
_offsetTextEditingController = TextEditingController(
text: widget.initialValue.offset.toString(),
);
}
@override
Widget build(BuildContext context) {
return FormBuilderField<RelativeDateRangeQuery>(
name: widget.name,
initialValue: widget.initialValue,
onChanged: widget.onChanged?.call,
builder: (field) => Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Last"),
SizedBox(
width: 70,
child: TextFormField(
decoration: InputDecoration(
labelText: "Offset",
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
validator: FormBuilderValidators.numeric(),
keyboardType: TextInputType.number,
onChanged: (value) {
final parsed = int.tryParse(value);
if (parsed != null) {
setState(() {
_offset = parsed;
});
field.didChange((field.value)?.copyWith(offset: parsed));
}
},
controller: _offsetTextEditingController,
),
),
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",
),
),
),
],
),
RelativeDateRangePickerHelper(
field: field,
onChanged: (value) {
if (value is RelativeDateRangeQuery) {
setState(() => _offset = value.offset);
_offsetTextEditingController.text = _offset.toString();
}
},
),
],
),
);
}
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);
}
}
}

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class RelativeDateRangePickerHelper extends StatefulWidget {
final FormFieldState<DateRangeQuery> field;
final void Function(DateRangeQuery value)? onChanged;
const RelativeDateRangePickerHelper({
super.key,
required this.field,
this.onChanged,
});
@override
State<RelativeDateRangePickerHelper> createState() =>
_RelativeDateRangePickerHelperState();
}
class _RelativeDateRangePickerHelperState
extends State<RelativeDateRangePickerHelper> {
@override
Widget build(BuildContext context) {
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) {
final value =
isSelected ? option.value : const RelativeDateRangeQuery();
widget.field.didChange(value);
widget.onChanged?.call(value);
},
selected: widget.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),
),
];
}
class _ExtendedDateRangeQueryOption {
final String title;
final RelativeDateRangeQuery value;
_ExtendedDateRangeQueryOption(this.title, this.value);
}

View File

@@ -0,0 +1,285 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
extension on Color {
/// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#".
/*static Color fromHex(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}*/
/// Prefixes a hash sign if [leadingHashSign] is set to `true` (default is `true`).
String toHex({bool leadingHashSign = true}) {
/// Converts an rgba value (0-255) into a 2-digit Hex code.
String hexValue(int rgbaVal) {
assert(rgbaVal == rgbaVal & 0xFF);
return rgbaVal.toRadixString(16).padLeft(2, '0').toUpperCase();
}
return '${leadingHashSign ? '#' : ''}'
'${hexValue(alpha)}${hexValue(red)}${hexValue(green)}${hexValue(blue)}';
}
}
enum ColorPickerType { colorPicker, materialPicker, blockPicker }
/// Creates a field for `Color` input selection
class FormBuilderColorPickerField extends FormBuilderField<Color> {
//TODO: Add documentation
final TextEditingController? controller;
final ColorPickerType colorPickerType;
final TextCapitalization textCapitalization;
final TextAlign textAlign;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final TextStyle? style;
final StrutStyle? strutStyle;
final TextDirection? textDirection;
final bool autofocus;
final bool obscureText;
final bool autocorrect;
final MaxLengthEnforcement? maxLengthEnforcement;
final int maxLines;
final bool expands;
final bool showCursor;
final int? minLines;
final int? maxLength;
final VoidCallback? onEditingComplete;
final ValueChanged<Color>? onFieldSubmitted;
final List<TextInputFormatter>? inputFormatters;
final double cursorWidth;
final Radius? cursorRadius;
final Color? cursorColor;
final Brightness? keyboardAppearance;
final EdgeInsets scrollPadding;
final bool enableInteractiveSelection;
final InputCounterWidgetBuilder? buildCounter;
final Widget Function(Color?)? colorPreviewBuilder;
FormBuilderColorPickerField({
Key? key,
//From Super
required String name,
FormFieldValidator<Color>? validator,
Color? initialValue,
InputDecoration decoration = const InputDecoration(),
ValueChanged<Color?>? onChanged,
ValueTransformer<Color?>? valueTransformer,
bool enabled = true,
FormFieldSetter<Color>? onSaved,
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
VoidCallback? onReset,
FocusNode? focusNode,
bool readOnly = false,
this.colorPickerType = ColorPickerType.colorPicker,
this.textCapitalization = TextCapitalization.none,
this.textAlign = TextAlign.start,
this.keyboardType,
this.textInputAction,
this.style,
this.strutStyle,
this.textDirection,
this.autofocus = false,
this.obscureText = false,
this.autocorrect = true,
this.maxLengthEnforcement,
this.maxLines = 1,
this.expands = false,
this.showCursor = false,
this.minLines,
this.maxLength,
this.onEditingComplete,
this.onFieldSubmitted,
this.inputFormatters,
this.cursorWidth = 2.0,
this.cursorRadius,
this.cursorColor,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.enableInteractiveSelection = true,
this.buildCounter,
this.controller,
this.colorPreviewBuilder,
}) : super(
key: key,
initialValue: initialValue,
name: name,
validator: validator,
valueTransformer: valueTransformer,
onChanged: onChanged,
autovalidateMode: autovalidateMode,
onSaved: onSaved,
enabled: enabled,
onReset: onReset,
decoration: decoration,
focusNode: focusNode,
builder: (FormFieldState<Color?> field) {
final state = field as FormBuilderColorPickerFieldState;
return TextField(
style: style,
decoration: state.decoration.copyWith(
suffixIcon: colorPreviewBuilder != null
? colorPreviewBuilder(field.value)
: LayoutBuilder(
key: ObjectKey(state.value),
builder: (context, constraints) {
return Icon(
Icons.circle,
key: ObjectKey(state.value),
size: constraints.minHeight,
color: state.value,
);
},
),
),
enabled: state.enabled,
readOnly: readOnly,
controller: state._effectiveController,
focusNode: state.effectiveFocusNode,
textAlign: textAlign,
autofocus: autofocus,
expands: expands,
scrollPadding: scrollPadding,
autocorrect: autocorrect,
textCapitalization: textCapitalization,
keyboardType: keyboardType,
obscureText: obscureText,
buildCounter: buildCounter,
cursorColor: cursorColor,
cursorRadius: cursorRadius,
cursorWidth: cursorWidth,
enableInteractiveSelection: enableInteractiveSelection,
inputFormatters: inputFormatters,
keyboardAppearance: keyboardAppearance,
maxLength: maxLength,
maxLengthEnforcement: maxLengthEnforcement,
maxLines: maxLines,
minLines: minLines,
onEditingComplete: onEditingComplete,
showCursor: showCursor,
strutStyle: strutStyle,
textDirection: textDirection,
textInputAction: textInputAction,
);
},
);
@override
FormBuilderColorPickerFieldState createState() =>
FormBuilderColorPickerFieldState();
}
class FormBuilderColorPickerFieldState
extends FormBuilderFieldState<FormBuilderColorPickerField, Color> {
late TextEditingController _effectiveController;
String? get valueString => value?.toHex();
Color? _selectedColor;
@override
void initState() {
super.initState();
_effectiveController = widget.controller ?? TextEditingController();
_effectiveController.text = valueString ?? '';
effectiveFocusNode.addListener(_handleFocus);
}
@override
void dispose() {
effectiveFocusNode.removeListener(_handleFocus);
// Dispose the _effectiveController when initState created it
if (null == widget.controller) {
_effectiveController.dispose();
}
super.dispose();
}
Future<void> _handleFocus() async {
if (effectiveFocusNode.hasFocus && enabled) {
effectiveFocusNode.unfocus();
final selected = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
final materialLocalizations = MaterialLocalizations.of(context);
return AlertDialog(
// title: null, //const Text('Pick a color!'),
content: SingleChildScrollView(
child: _buildColorPicker(),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(materialLocalizations.cancelButtonLabel),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text(materialLocalizations.okButtonLabel),
),
],
);
},
);
if (true == selected) {
didChange(_selectedColor);
}
}
}
Widget _buildColorPicker() {
switch (widget.colorPickerType) {
case ColorPickerType.colorPicker:
return ColorPicker(
pickerColor: value ?? Colors.transparent,
onColorChanged: _colorChanged,
colorPickerWidth: 300,
displayThumbColor: true,
enableAlpha: true,
paletteType: PaletteType.hsl,
pickerAreaHeightPercent: 1.0,
);
case ColorPickerType.materialPicker:
return MaterialPicker(
pickerColor: value ?? Colors.transparent,
onColorChanged: _colorChanged,
enableLabel: true, // only on portrait mode
);
case ColorPickerType.blockPicker:
return BlockPicker(
pickerColor: value ?? Colors.transparent,
onColorChanged: _colorChanged,
);
default:
throw 'Unknown ColorPickerType';
}
}
void _colorChanged(Color color) => _selectedColor = color;
void _setTextFieldString() => _effectiveController.text = valueString ?? '';
@override
void didChange(Color? value) {
super.didChange(value);
_setTextFieldString();
}
@override
void reset() {
super.reset();
_setTextFieldString();
}
}

View File

@@ -1,102 +0,0 @@
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);
}
}
}

View File

@@ -0,0 +1,430 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
typedef SelectionToTextTransformer<T> = String Function(T suggestion);
/// Text field that auto-completes user input from a list of items
class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
/// Called with the search pattern to get the search suggestions.
///
/// This callback must not be null. It is be called by the TypeAhead widget
/// and provided with the search pattern. It should return a [List](https://api.dartlang.org/stable/2.0.0/dart-core/List-class.html)
/// of suggestions either synchronously, or asynchronously (as the result of a
/// [Future](https://api.dartlang.org/stable/dart-async/Future-class.html)).
/// Typically, the list of suggestions should not contain more than 4 or 5
/// entries. These entries will then be provided to [itemBuilder] to display
/// the suggestions.
///
/// Example:
/// ```dart
/// suggestionsCallback: (pattern) async {
/// return await _getSuggestions(pattern);
/// }
/// ```
final SuggestionsCallback<T> suggestionsCallback;
/// Called when a suggestion is tapped.
///
/// This callback must not be null. It is called by the TypeAhead widget and
/// provided with the value of the tapped suggestion.
///
/// For example, you might want to navigate to a specific view when the user
/// tabs a suggestion:
/// ```dart
/// onSuggestionSelected: (suggestion) {
/// Navigator.of(context).push(MaterialPageRoute(
/// builder: (context) => SearchResult(
/// searchItem: suggestion
/// )
/// ));
/// }
/// ```
///
/// Or to set the value of the text field:
/// ```dart
/// onSuggestionSelected: (suggestion) {
/// _controller.text = suggestion['name'];
/// }
/// ```
final SuggestionSelectionCallback<T>? onSuggestionSelected;
/// Called for each suggestion returned by [suggestionsCallback] to build the
/// corresponding widget.
///
/// This callback must not be null. It is called by the TypeAhead widget for
/// each suggestion, and expected to build a widget to display this
/// suggestion's info. For example:
///
/// ```dart
/// itemBuilder: (context, suggestion) {
/// return ListTile(
/// title: Text(suggestion['name']),
/// subtitle: Text('USD' + suggestion['price'].toString())
/// );
/// }
/// ```
final ItemBuilder<T> itemBuilder;
/// The decoration of the material sheet that contains the suggestions.
///
/// If null, default decoration with an elevation of 4.0 is used
final SuggestionsBoxDecoration suggestionsBoxDecoration;
/// Used to control the `_SuggestionsBox`. Allows manual control to
/// open, close, toggle, or resize the `_SuggestionsBox`.
final SuggestionsBoxController? suggestionsBoxController;
/// The duration to wait after the user stops typing before calling
/// [suggestionsCallback]
///
/// This is useful, because, if not set, a request for suggestions will be
/// sent for every character that the user types.
///
/// This duration is set by default to 300 milliseconds
final Duration debounceDuration;
/// Called when waiting for [suggestionsCallback] to return.
///
/// It is expected to return a widget to display while waiting.
/// For example:
/// ```dart
/// (BuildContext context) {
/// return Text('Loading...');
/// }
/// ```
///
/// If not specified, a [CircularProgressIndicator](https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html) is shown
final WidgetBuilder? loadingBuilder;
/// Called when [suggestionsCallback] returns an empty array.
///
/// It is expected to return a widget to display when no suggestions are
/// available.
/// For example:
/// ```dart
/// (BuildContext context) {
/// return Text('No Items Found!');
/// }
/// ```
///
/// If not specified, a simple text is shown
final WidgetBuilder? noItemsFoundBuilder;
/// Called when [suggestionsCallback] throws an exception.
///
/// It is called with the error object, and expected to return a widget to
/// display when an exception is thrown
/// For example:
/// ```dart
/// (BuildContext context, error) {
/// return Text('$error');
/// }
/// ```
///
/// If not specified, the error is shown in [ThemeData.errorColor](https://docs.flutter.io/flutter/material/ThemeData/errorColor.html)
final ErrorBuilder? errorBuilder;
/// Called to display animations when [suggestionsCallback] returns suggestions
///
/// It is provided with the suggestions box instance and the animation
/// controller, and expected to return some animation that uses the controller
/// to display the suggestion box.
///
/// For example:
/// ```dart
/// transitionBuilder: (context, suggestionsBox, animationController) {
/// return FadeTransition(
/// child: suggestionsBox,
/// opacity: CurvedAnimation(
/// parent: animationController,
/// curve: Curves.fastOutSlowIn
/// ),
/// );
/// }
/// ```
/// This argument is best used with [animationDuration] and [animationStart]
/// to fully control the animation.
///
/// To fully remove the animation, just return `suggestionsBox`
///
/// If not specified, a [SizeTransition](https://docs.flutter.io/flutter/widgets/SizeTransition-class.html) is shown.
final AnimationTransitionBuilder? transitionBuilder;
/// The duration that [transitionBuilder] animation takes.
///
/// This argument is best used with [transitionBuilder] and [animationStart]
/// to fully control the animation.
///
/// Defaults to 500 milliseconds.
final Duration animationDuration;
/// Determine the [SuggestionBox]'s direction.
///
/// If [AxisDirection.down], the [SuggestionBox] will be below the [TextField]
/// and the [_SuggestionsList] will grow **down**.
///
/// If [AxisDirection.up], the [SuggestionBox] will be above the [TextField]
/// and the [_SuggestionsList] will grow **up**.
///
/// [AxisDirection.left] and [AxisDirection.right] are not allowed.
final AxisDirection direction;
/// The value at which the [transitionBuilder] animation starts.
///
/// This argument is best used with [transitionBuilder] and [animationDuration]
/// to fully control the animation.
///
/// Defaults to 0.25.
final double animationStart;
/// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
/// that the TypeAhead widget displays
final TextFieldConfiguration textFieldConfiguration;
/// How far below the text field should the suggestions box be
///
/// Defaults to 5.0
final double suggestionsBoxVerticalOffset;
/// If set to true, suggestions will be fetched immediately when the field is
/// added to the view.
///
/// But the suggestions box will only be shown when the field receives focus.
/// To make the field receive focus immediately, you can set the `autofocus`
/// property in the [textFieldConfiguration] to true
///
/// Defaults to false
final bool getImmediateSuggestions;
/// If set to true, no loading box will be shown while suggestions are
/// being fetched. [loadingBuilder] will also be ignored.
///
/// Defaults to false.
final bool hideOnLoading;
/// If set to true, nothing will be shown if there are no results.
/// [noItemsFoundBuilder] will also be ignored.
///
/// Defaults to false.
final bool hideOnEmpty;
/// If set to true, nothing will be shown if there is an error.
/// [errorBuilder] will also be ignored.
///
/// Defaults to false.
final bool hideOnError;
/// If set to false, the suggestions box will stay opened after
/// the keyboard is closed.
///
/// Defaults to true.
final bool hideSuggestionsOnKeyboardHide;
/// If set to false, the suggestions box will show a circular
/// progress indicator when retrieving suggestions.
///
/// Defaults to true.
final bool keepSuggestionsOnLoading;
/// If set to true, the suggestions box will remain opened even after
/// selecting a suggestion.
///
/// Note that if this is enabled, the only way
/// to close the suggestions box is either manually via the
/// `SuggestionsBoxController` or when the user closes the software
/// keyboard if `hideSuggestionsOnKeyboardHide` is set to true. Users
/// with a physical keyboard will be unable to close the
/// box without a manual way via `SuggestionsBoxController`.
///
/// Defaults to false.
final bool keepSuggestionsOnSuggestionSelected;
/// If set to true, in the case where the suggestions box has less than
/// _SuggestionsBoxController.minOverlaySpace to grow in the desired [direction], the direction axis
/// will be temporarily flipped if there's more room available in the opposite
/// direction.
///
/// Defaults to false
final bool autoFlipDirection;
final SelectionToTextTransformer<T>? selectionToTextTransformer;
/// Controls the text being edited.
///
/// If null, this widget will create its own [TextEditingController].
final TextEditingController? controller;
final bool hideKeyboard;
final ScrollController? scrollController;
/// Creates text field that auto-completes user input from a list of items
FormBuilderTypeAhead({
Key? key,
//From Super
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
bool enabled = true,
FocusNode? focusNode,
FormFieldSetter<T>? onSaved,
FormFieldValidator<T>? validator,
InputDecoration decoration = const InputDecoration(),
required String name,
required this.itemBuilder,
required this.suggestionsCallback,
T? initialValue,
ValueChanged<T?>? onChanged,
ValueTransformer<T?>? valueTransformer,
VoidCallback? onReset,
this.animationDuration = const Duration(milliseconds: 500),
this.animationStart = 0.25,
this.autoFlipDirection = false,
this.controller,
this.debounceDuration = const Duration(milliseconds: 300),
this.direction = AxisDirection.down,
this.errorBuilder,
this.getImmediateSuggestions = false,
this.hideKeyboard = false,
this.hideOnEmpty = false,
this.hideOnError = false,
this.hideOnLoading = false,
this.hideSuggestionsOnKeyboardHide = true,
this.keepSuggestionsOnLoading = true,
this.keepSuggestionsOnSuggestionSelected = false,
this.loadingBuilder,
this.noItemsFoundBuilder,
this.onSuggestionSelected,
this.scrollController,
this.selectionToTextTransformer,
this.suggestionsBoxController,
this.suggestionsBoxDecoration = const SuggestionsBoxDecoration(),
this.suggestionsBoxVerticalOffset = 5.0,
this.textFieldConfiguration = const TextFieldConfiguration(),
this.transitionBuilder,
}) : assert(T == String || selectionToTextTransformer != null),
super(
key: key,
initialValue: initialValue,
name: name,
validator: validator,
valueTransformer: valueTransformer,
onChanged: onChanged,
autovalidateMode: autovalidateMode,
onSaved: onSaved,
enabled: enabled,
onReset: onReset,
decoration: decoration,
focusNode: focusNode,
builder: (FormFieldState<T?> field) {
final state = field as FormBuilderTypeAheadState<T>;
final theme = Theme.of(state.context);
return TypeAheadField<T>(
textFieldConfiguration: textFieldConfiguration.copyWith(
enabled: state.enabled,
controller: state._typeAheadController,
style: state.enabled
? textFieldConfiguration.style
: theme.textTheme.titleMedium!.copyWith(
color: theme.disabledColor,
),
focusNode: state.effectiveFocusNode,
decoration: state.decoration,
),
// TODO HACK to satisfy strictness
suggestionsCallback: suggestionsCallback,
itemBuilder: itemBuilder,
transitionBuilder: (context, suggestionsBox, controller) =>
suggestionsBox,
onSuggestionSelected: (T suggestion) {
state.didChange(suggestion);
onSuggestionSelected?.call(suggestion);
},
getImmediateSuggestions: getImmediateSuggestions,
errorBuilder: errorBuilder,
noItemsFoundBuilder: noItemsFoundBuilder,
loadingBuilder: loadingBuilder,
debounceDuration: debounceDuration,
suggestionsBoxDecoration: suggestionsBoxDecoration,
suggestionsBoxVerticalOffset: suggestionsBoxVerticalOffset,
animationDuration: animationDuration,
animationStart: animationStart,
direction: direction,
hideOnLoading: hideOnLoading,
hideOnEmpty: hideOnEmpty,
hideOnError: hideOnError,
hideSuggestionsOnKeyboardHide: hideSuggestionsOnKeyboardHide,
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
autoFlipDirection: autoFlipDirection,
suggestionsBoxController: suggestionsBoxController,
keepSuggestionsOnSuggestionSelected:
keepSuggestionsOnSuggestionSelected,
hideKeyboard: hideKeyboard,
scrollController: scrollController,
);
},
);
@override
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<T>();
}
class FormBuilderTypeAheadState<T>
extends FormBuilderFieldState<FormBuilderTypeAhead<T>, T> {
late TextEditingController _typeAheadController;
@override
void initState() {
super.initState();
_typeAheadController = widget.controller ??
TextEditingController(text: _getTextString(initialValue));
// _typeAheadController.addListener(_handleControllerChanged);
}
// void _handleControllerChanged() {
// Suppress changes that originated from within this class.
//
// In the case where a controller has been passed in to this widget, we
// register this change listener. In these cases, we'll also receive change
// notifications for changes originating from within this class -- for
// example, the reset() method. In such cases, the FormField value will
// already have been set.
// if (_typeAheadController.text != value) {
// didChange(_typeAheadController.text as T);
// }
// }
@override
void didChange(T? value) {
super.didChange(value);
var text = _getTextString(value);
if (_typeAheadController.text != text) {
_typeAheadController.text = text;
}
}
@override
void dispose() {
// Dispose the _typeAheadController when initState created it
super.dispose();
_typeAheadController.dispose();
}
@override
void reset() {
super.reset();
_typeAheadController.text = _getTextString(initialValue);
}
String _getTextString(T? value) {
var text = value == null
? ''
: widget.selectionToTextTransformer != null
? widget.selectionToTextTransformer!(value)
: value.toString();
return text;
}
}

View File

@@ -1,4 +1,3 @@
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
@@ -83,16 +82,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
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,
return Badge.count(
alignment: const AlignmentDirectional(44,
-4), //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
isLabelVisible: appliedFiltersCount > 0,
count: state.filter.appliedFiltersCount,
child: FloatingActionButton(
child: const Icon(Icons.filter_alt_rounded),
onPressed: _openDocumentFilter,

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.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/form_builder_extended_date_range_picker.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/widgets/search/query_type_form_field.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
@@ -101,8 +101,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
initialValue: widget.initialFilter.created,
labelText: S.of(context).documentCreatedPropertyLabel,
).padded(),
// _buildCreatedDateRangePickerFormField(),
// _buildAddedDateRangePickerFormField(),
FormBuilderExtendedDateRangePicker(
name: DocumentModel.addedKey,
initialValue: widget.initialFilter.added,
labelText: S.of(context).documentAddedPropertyLabel,
).padded(),
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
@@ -209,100 +212,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
// Widget _buildCreatedDateRangePickerFormField() {
// return Column(
// children: [
// FormBuilderDateRangePicker(
// initialValue: _dateTimeRangeOfNullable(
// widget.initialFilter.createdDateAfter,
// widget.initialFilter.createdDateBefore,
// ),
// // Workaround for theme data not being correctly passed to daterangepicker, see
// // https://github.com/flutter/flutter/issues/87580
// pickerBuilder: (context, Widget? child) => Theme(
// data: Theme.of(context).copyWith(
// dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
// appBarTheme: Theme.of(context).appBarTheme.copyWith(
// iconTheme:
// IconThemeData(color: Theme.of(context).primaryColor),
// ),
// colorScheme: Theme.of(context).colorScheme.copyWith(
// onPrimary: Theme.of(context).primaryColor,
// primary: Theme.of(context).colorScheme.primary,
// ),
// ),
// child: child!,
// ),
// format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
// fieldStartLabelText:
// S.of(context).documentFilterDateRangeFieldStartLabel,
// fieldEndLabelText: S.of(context).documentFilterDateRangeFieldEndLabel,
// firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
// lastDate: DateTime.now(),
// name: fkCreatedAt,
// decoration: InputDecoration(
// prefixIcon: const Icon(Icons.calendar_month_outlined),
// labelText: S.of(context).documentCreatedPropertyLabel,
// suffixIcon: IconButton(
// icon: const Icon(Icons.clear),
// onPressed: () {
// _formKey.currentState?.fields[fkCreatedAt]?.didChange(null);
// },
// ),
// ),
// ).paddedSymmetrically(horizontal: 8, vertical: 4.0),
// ],
// );
// }
// Widget _buildAddedDateRangePickerFormField() {
// return Column(
// children: [
// FormBuilderDateRangePicker(
// initialValue: _dateTimeRangeOfNullable(
// widget.initialFilter.addedDateAfter,
// widget.initialFilter.addedDateBefore,
// ),
// // Workaround for theme data not being correctly passed to daterangepicker, see
// // https://github.com/flutter/flutter/issues/87580
// pickerBuilder: (context, Widget? child) => Theme(
// data: Theme.of(context).copyWith(
// dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
// appBarTheme: Theme.of(context).appBarTheme.copyWith(
// iconTheme:
// IconThemeData(color: Theme.of(context).primaryColor),
// ),
// colorScheme: Theme.of(context).colorScheme.copyWith(
// onPrimary: Theme.of(context).primaryColor,
// primary: Theme.of(context).colorScheme.primary,
// ),
// ),
// child: child!,
// ),
// format: DateFormat.yMMMd(),
// fieldStartLabelText:
// S.of(context).documentFilterDateRangeFieldStartLabel,
// fieldEndLabelText: S.of(context).documentFilterDateRangeFieldEndLabel,
// firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
// lastDate: DateTime.now(),
// name: fkAddedAt,
// decoration: InputDecoration(
// prefixIcon: const Icon(Icons.calendar_month_outlined),
// labelText: S.of(context).documentAddedPropertyLabel,
// suffixIcon: IconButton(
// icon: const Icon(Icons.clear),
// onPressed: () {
// _formKey.currentState?.fields[fkAddedAt]?.didChange(null);
// },
// ),
// ),
// ).paddedSymmetrically(horizontal: 8),
// const SizedBox(height: 4.0),
// _buildDateRangePickerHelper(fkAddedAt),
// ],
// );
// }
void _onApplyFilter() async {
_formKey.currentState?.save();
if (_formKey.currentState?.validate() ?? false) {
@@ -320,7 +229,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
DocumentFilter _assembleFilter() {
final v = _formKey.currentState!.value;
return DocumentFilter(
created: (v[fkCreatedAt] as DateRangeQuery),
correspondent: v[fkCorrespondent] as IdQueryParameter? ??
DocumentFilter.initial.correspondent,
documentType: v[fkDocumentType] as IdQueryParameter? ??
@@ -330,6 +238,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
tags:
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
queryText: v[fkQuery] as String?,
created: (v[fkCreatedAt] as DateRangeQuery),
added: (v[fkAddedAt] as DateRangeQuery),
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
asnQuery: widget.initialFilter.asnQuery,

View File

@@ -3,9 +3,9 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';

View File

@@ -27,6 +27,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
@override
void initState() {
super.initState();

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_type_ahead.dart';
import 'package:paperless_mobile/generated/l10n.dart';
///

View File

@@ -56,10 +56,10 @@ class RelativeDateRangeQuery extends DateRangeQuery {
final int offset;
final DateRangeUnit unit;
const RelativeDateRangeQuery(
this.offset,
this.unit,
);
const RelativeDateRangeQuery([
this.offset = 1,
this.unit = DateRangeUnit.day,
]);
@override
List<Object?> get props => [offset, unit];
@@ -67,7 +67,7 @@ class RelativeDateRangeQuery extends DateRangeQuery {
@override
Map<String, String> toQueryParameter(DateRangeQueryField field) {
return {
'query': '[${field.name}:$offset ${unit.name} to now]',
'query': '${field.name}:[-$offset ${unit.name} to now]',
};
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -58,10 +58,6 @@ dependencies:
flutter_bloc: ^8.1.1
equatable: ^2.0.3
flutter_form_builder: ^7.5.0
form_builder_extra_fields:
git:
url: https://github.com/flutter-form-builder-ecosystem/form_builder_extra_fields.git
ref: main
form_builder_validators: ^8.4.0
infinite_scroll_pagination: ^3.2.0
package_info_plus: ^1.4.3+1
@@ -82,6 +78,7 @@ dependencies:
hive: ^2.2.3
rxdart: ^0.27.7
badges: ^2.0.3
flutter_colorpicker: ^1.0.3
dev_dependencies:
integration_test: