mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 18:08:07 -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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.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_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/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
|
||||||
class ExtendedDateRangeDialog extends StatefulWidget {
|
class ExtendedDateRangeDialog extends StatefulWidget {
|
||||||
final DateRangeQuery initialValue;
|
final DateRangeQuery initialValue;
|
||||||
final String Function(DateRangeQuery query) stringTransformer;
|
|
||||||
const ExtendedDateRangeDialog({
|
const ExtendedDateRangeDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.initialValue,
|
required this.initialValue,
|
||||||
required this.stringTransformer,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -26,12 +29,21 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
|
|||||||
static const String _fkAbsoluteAfter = 'absoluteAfter';
|
static const String _fkAbsoluteAfter = 'absoluteAfter';
|
||||||
static const String _fkRelative = 'relative';
|
static const String _fkRelative = 'relative';
|
||||||
|
|
||||||
|
DateTime? _before;
|
||||||
|
DateTime? _after;
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
late DateRangeType _selectedDateRangeType;
|
late DateRangeType _selectedDateRangeType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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.relative
|
||||||
: DateRangeType.absolute;
|
: DateRangeType.absolute;
|
||||||
}
|
}
|
||||||
@@ -40,13 +52,13 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text("Select date range"),
|
title: Text("Select date range"),
|
||||||
content: Form(
|
content: FormBuilder(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
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,
|
style: Theme.of(context).textTheme.caption,
|
||||||
),
|
),
|
||||||
_buildDateRangeQueryTypeSelection(),
|
_buildDateRangeQueryTypeSelection(),
|
||||||
@@ -58,6 +70,7 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
|
|||||||
return _buildAbsoluteDateRangeForm();
|
return _buildAbsoluteDateRangeForm();
|
||||||
case DateRangeType.relative:
|
case DateRangeType.relative:
|
||||||
return FormBuilderRelativeDateRangePicker(
|
return FormBuilderRelativeDateRangePicker(
|
||||||
|
name: _fkRelative,
|
||||||
initialValue:
|
initialValue:
|
||||||
widget.initialValue is RelativeDateRangeQuery
|
widget.initialValue is RelativeDateRangeQuery
|
||||||
? widget.initialValue as RelativeDateRangeQuery
|
? widget.initialValue as RelativeDateRangeQuery
|
||||||
@@ -65,7 +78,6 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
|
|||||||
1,
|
1,
|
||||||
DateRangeUnit.month,
|
DateRangeUnit.month,
|
||||||
),
|
),
|
||||||
name: _fkRelative,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -118,23 +130,58 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
|
|||||||
children: [
|
children: [
|
||||||
FormBuilderDateTimePicker(
|
FormBuilderDateTimePicker(
|
||||||
name: _fkAbsoluteAfter,
|
name: _fkAbsoluteAfter,
|
||||||
initialDate: widget.initialValue is AbsoluteDateRangeQuery
|
initialValue: widget.initialValue is AbsoluteDateRangeQuery
|
||||||
? (widget.initialValue as AbsoluteDateRangeQuery).after
|
? (widget.initialValue as AbsoluteDateRangeQuery).after
|
||||||
: null,
|
: null,
|
||||||
|
initialDate: _before?.subtract(const Duration(days: 1)),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: S.of(context).extendedDateRangePickerAfterLabel,
|
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,
|
inputType: InputType.date,
|
||||||
|
onChanged: (after) {
|
||||||
|
setState(() => _after = after);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
FormBuilderDateTimePicker(
|
FormBuilderDateTimePicker(
|
||||||
name: _fkAbsoluteBefore,
|
name: _fkAbsoluteBefore,
|
||||||
initialDate: widget.initialValue is AbsoluteDateRangeQuery
|
initialValue: widget.initialValue is AbsoluteDateRangeQuery
|
||||||
? (widget.initialValue as AbsoluteDateRangeQuery).before
|
? (widget.initialValue as AbsoluteDateRangeQuery).before
|
||||||
: null,
|
: null,
|
||||||
inputType: InputType.date,
|
inputType: InputType.date,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: S.of(context).extendedDateRangePickerBeforeLabel,
|
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;
|
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 {
|
enum DateRangeType {
|
||||||
@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.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';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
class FormBuilderExtendedDateRangePicker extends StatefulWidget {
|
class FormBuilderExtendedDateRangePicker extends StatefulWidget {
|
||||||
@@ -24,13 +25,13 @@ class FormBuilderExtendedDateRangePicker extends StatefulWidget {
|
|||||||
|
|
||||||
class _FormBuilderExtendedDateRangePickerState
|
class _FormBuilderExtendedDateRangePickerState
|
||||||
extends State<FormBuilderExtendedDateRangePicker> {
|
extends State<FormBuilderExtendedDateRangePicker> {
|
||||||
late final TextEditingController _textEditingController;
|
final TextEditingController _textEditingController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void didChangeDependencies() {
|
||||||
super.initState();
|
super.didChangeDependencies();
|
||||||
_textEditingController = TextEditingController(
|
// This has to be initialized here and not in initState because it has to be waited until dependencies for localization have been loaded.
|
||||||
text: _dateRangeQueryToString(widget.initialValue));
|
_textEditingController.text = _dateRangeQueryToString(widget.initialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -52,67 +53,39 @@ class _FormBuilderExtendedDateRangePickerState
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixIcon: const Icon(Icons.date_range),
|
prefixIcon: const Icon(Icons.date_range),
|
||||||
labelText: widget.labelText,
|
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) {
|
String _dateRangeQueryToString(DateRangeQuery query) {
|
||||||
|
final df = DateFormat.yMd();
|
||||||
if (query is UnsetDateRangeQuery) {
|
if (query is UnsetDateRangeQuery) {
|
||||||
return '';
|
return '';
|
||||||
} else if (query is AbsoluteDateRangeQuery) {
|
} else if (query is AbsoluteDateRangeQuery) {
|
||||||
if (query.before != null && query.after != null) {
|
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) {
|
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) {
|
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) {
|
} else if (query is RelativeDateRangeQuery) {
|
||||||
switch (query.unit) {
|
switch (query.unit) {
|
||||||
@@ -143,20 +116,10 @@ class _FormBuilderExtendedDateRangePickerState
|
|||||||
) async {
|
) async {
|
||||||
final query = await showDialog<DateRangeQuery>(
|
final query = await showDialog<DateRangeQuery>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ExtendedDateRangeDialog(
|
builder: (context) => ExtendedDateRangeDialog(initialValue: field.value!),
|
||||||
initialValue: field.value!,
|
|
||||||
stringTransformer: _dateRangeQueryToString,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (query != null) {
|
if (query != null) {
|
||||||
field.didChange(query);
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
@@ -83,16 +82,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||||
return Badge(
|
return Badge.count(
|
||||||
toAnimate: false,
|
alignment: const AlignmentDirectional(44,
|
||||||
animationType: BadgeAnimationType.fade,
|
-4), //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
|
||||||
showBadge: appliedFiltersCount > 0,
|
isLabelVisible: appliedFiltersCount > 0,
|
||||||
badgeContent: appliedFiltersCount > 0
|
count: state.filter.appliedFiltersCount,
|
||||||
? Text(
|
|
||||||
state.filter.appliedFiltersCount.toString(),
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
child: const Icon(Icons.filter_alt_rounded),
|
child: const Icon(Icons.filter_alt_rounded),
|
||||||
onPressed: _openDocumentFilter,
|
onPressed: _openDocumentFilter,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.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/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.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';
|
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||||
@@ -101,8 +101,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
initialValue: widget.initialFilter.created,
|
initialValue: widget.initialFilter.created,
|
||||||
labelText: S.of(context).documentCreatedPropertyLabel,
|
labelText: S.of(context).documentCreatedPropertyLabel,
|
||||||
).padded(),
|
).padded(),
|
||||||
// _buildCreatedDateRangePickerFormField(),
|
FormBuilderExtendedDateRangePicker(
|
||||||
// _buildAddedDateRangePickerFormField(),
|
name: DocumentModel.addedKey,
|
||||||
|
initialValue: widget.initialFilter.added,
|
||||||
|
labelText: S.of(context).documentAddedPropertyLabel,
|
||||||
|
).padded(),
|
||||||
_buildCorrespondentFormField().padded(),
|
_buildCorrespondentFormField().padded(),
|
||||||
_buildDocumentTypeFormField().padded(),
|
_buildDocumentTypeFormField().padded(),
|
||||||
_buildStoragePathFormField().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 {
|
void _onApplyFilter() async {
|
||||||
_formKey.currentState?.save();
|
_formKey.currentState?.save();
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
@@ -320,7 +229,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
DocumentFilter _assembleFilter() {
|
DocumentFilter _assembleFilter() {
|
||||||
final v = _formKey.currentState!.value;
|
final v = _formKey.currentState!.value;
|
||||||
return DocumentFilter(
|
return DocumentFilter(
|
||||||
created: (v[fkCreatedAt] as DateRangeQuery),
|
|
||||||
correspondent: v[fkCorrespondent] as IdQueryParameter? ??
|
correspondent: v[fkCorrespondent] as IdQueryParameter? ??
|
||||||
DocumentFilter.initial.correspondent,
|
DocumentFilter.initial.correspondent,
|
||||||
documentType: v[fkDocumentType] as IdQueryParameter? ??
|
documentType: v[fkDocumentType] as IdQueryParameter? ??
|
||||||
@@ -330,6 +238,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
tags:
|
tags:
|
||||||
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
||||||
queryText: v[fkQuery] as String?,
|
queryText: v[fkQuery] as String?,
|
||||||
|
created: (v[fkCreatedAt] as DateRangeQuery),
|
||||||
added: (v[fkAddedAt] as DateRangeQuery),
|
added: (v[fkAddedAt] as DateRangeQuery),
|
||||||
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
|
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
|
||||||
asnQuery: widget.initialFilter.asnQuery,
|
asnQuery: widget.initialFilter.asnQuery,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import 'dart:math';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.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/cubit/edit_label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.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/cubit/edit_label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class HomePage extends StatefulWidget {
|
|||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
|
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.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_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';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ class RelativeDateRangeQuery extends DateRangeQuery {
|
|||||||
final int offset;
|
final int offset;
|
||||||
final DateRangeUnit unit;
|
final DateRangeUnit unit;
|
||||||
|
|
||||||
const RelativeDateRangeQuery(
|
const RelativeDateRangeQuery([
|
||||||
this.offset,
|
this.offset = 1,
|
||||||
this.unit,
|
this.unit = DateRangeUnit.day,
|
||||||
);
|
]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [offset, unit];
|
List<Object?> get props => [offset, unit];
|
||||||
@@ -67,7 +67,7 @@ class RelativeDateRangeQuery extends DateRangeQuery {
|
|||||||
@override
|
@override
|
||||||
Map<String, String> toQueryParameter(DateRangeQueryField field) {
|
Map<String, String> toQueryParameter(DateRangeQueryField field) {
|
||||||
return {
|
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
|
flutter_bloc: ^8.1.1
|
||||||
equatable: ^2.0.3
|
equatable: ^2.0.3
|
||||||
flutter_form_builder: ^7.5.0
|
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
|
form_builder_validators: ^8.4.0
|
||||||
infinite_scroll_pagination: ^3.2.0
|
infinite_scroll_pagination: ^3.2.0
|
||||||
package_info_plus: ^1.4.3+1
|
package_info_plus: ^1.4.3+1
|
||||||
@@ -82,6 +78,7 @@ dependencies:
|
|||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
rxdart: ^0.27.7
|
rxdart: ^0.27.7
|
||||||
badges: ^2.0.3
|
badges: ^2.0.3
|
||||||
|
flutter_colorpicker: ^1.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
integration_test:
|
integration_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user