mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 14:07:49 -06:00
First working version of new date range picker
This commit is contained in:
@@ -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 {
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
///
|
||||
|
||||
@@ -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]',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
747
pubspec.lock
747
pubspec.lock
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user