mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 05:15:50 -06:00
feat: Add improved date input
This commit is contained in:
BIN
assets/fonts/RobotoMono-Regular.ttf
Normal file
BIN
assets/fonts/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
@@ -1,41 +1,26 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:extended_masked_text/extended_masked_text.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
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:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||||
import 'package:synchronized/extension.dart';
|
import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
final class NeighbourAwareDateInputSegmentControls
|
|
||||||
with LinkedListEntry<NeighbourAwareDateInputSegmentControls> {
|
|
||||||
final FocusNode node;
|
|
||||||
final TextEditingController controller;
|
|
||||||
final int position;
|
|
||||||
final String format;
|
|
||||||
final DateTime? initialDate;
|
|
||||||
|
|
||||||
NeighbourAwareDateInputSegmentControls({
|
|
||||||
required this.node,
|
|
||||||
required this.controller,
|
|
||||||
required this.format,
|
|
||||||
this.initialDate,
|
|
||||||
required this.position,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// A localized, segmented date input field.
|
||||||
class FormBuilderLocalizedDatePicker extends StatefulWidget {
|
class FormBuilderLocalizedDatePicker extends StatefulWidget {
|
||||||
final String name;
|
final String name;
|
||||||
|
final Locale locale;
|
||||||
final String labelText;
|
final String labelText;
|
||||||
final Widget? prefixIcon;
|
final Widget? prefixIcon;
|
||||||
final DateTime? initialValue;
|
final DateTime? initialValue;
|
||||||
final DateTime firstDate;
|
final DateTime firstDate;
|
||||||
final DateTime lastDate;
|
final DateTime lastDate;
|
||||||
final Locale locale;
|
|
||||||
|
/// If set to true, the field will not throw any validation errors when empty.
|
||||||
|
final bool allowUnset;
|
||||||
|
|
||||||
const FormBuilderLocalizedDatePicker({
|
const FormBuilderLocalizedDatePicker({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -46,6 +31,7 @@ class FormBuilderLocalizedDatePicker extends StatefulWidget {
|
|||||||
required this.locale,
|
required this.locale,
|
||||||
required this.labelText,
|
required this.labelText,
|
||||||
this.prefixIcon,
|
this.prefixIcon,
|
||||||
|
this.allowUnset = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -59,8 +45,9 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
late final String _format;
|
late final String _format;
|
||||||
|
|
||||||
final _textFieldControls =
|
final _textFieldControls =
|
||||||
LinkedList<NeighbourAwareDateInputSegmentControls>();
|
LinkedList<_NeighbourAwareDateInputSegmentControls>();
|
||||||
|
String? _error;
|
||||||
|
bool _temporarilyDisableListeners = false;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -78,29 +65,44 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
final initialText = widget.initialValue != null
|
final initialText = widget.initialValue != null
|
||||||
? DateFormat(formatString).format(widget.initialValue!)
|
? DateFormat(formatString).format(widget.initialValue!)
|
||||||
: null;
|
: null;
|
||||||
final item = NeighbourAwareDateInputSegmentControls(
|
final controls = _NeighbourAwareDateInputSegmentControls(
|
||||||
node: FocusNode(debugLabel: formatString),
|
node: FocusNode(debugLabel: formatString),
|
||||||
controller: TextEditingController(text: initialText),
|
controller: TextEditingController(text: initialText),
|
||||||
format: formatString,
|
format: formatString,
|
||||||
position: i,
|
position: i,
|
||||||
|
type: _DateInputSegment.fromPattern(formatString),
|
||||||
);
|
);
|
||||||
item.controller.addListener(() {
|
_textFieldControls.add(controls);
|
||||||
if (item.controller.text.length == item.format.length) {
|
controls.controller.addListener(() {
|
||||||
// _textFieldControls.elementAt(i).next?.node.requestFocus();
|
if (_temporarilyDisableListeners) {
|
||||||
// _textFieldControls.elementAt(i).next?.controller.selection =
|
return;
|
||||||
// const TextSelection.collapsed(offset: 0);
|
}
|
||||||
// return;
|
if (controls.controller.selection.isCollapsed &&
|
||||||
|
controls.controller.text.length == controls.format.length) {
|
||||||
|
controls.next?.node.requestFocus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
item.node.addListener(() {
|
controls.node.addListener(() {
|
||||||
if (item.node.hasFocus) {
|
if (_temporarilyDisableListeners || !controls.node.hasFocus) {
|
||||||
item.controller.selection = const TextSelection.collapsed(offset: 0);
|
return;
|
||||||
}
|
}
|
||||||
|
controls.controller.selection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: controls.controller.text.length,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
_textFieldControls.add(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (var controls in _textFieldControls) {
|
||||||
|
controls.node.dispose();
|
||||||
|
controls.controller.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RawKeyboardListener(
|
return RawKeyboardListener(
|
||||||
@@ -123,27 +125,50 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: FormBuilderField<DateTime>(
|
child: FormBuilderField<DateTime>(
|
||||||
name: widget.name,
|
validator: _validateDate,
|
||||||
initialValue: widget.initialValue,
|
onChanged: (value) {
|
||||||
validator: (value) {
|
// We have to temporarily disable our listeners on the TextEditingController here
|
||||||
|
// since otherwise the listeners get notified of the change and
|
||||||
|
// the fields get focused and highlighted/selected (as defined in the
|
||||||
|
// listeners above).
|
||||||
|
_temporarilyDisableListeners = true;
|
||||||
|
for (var control in _textFieldControls) {
|
||||||
|
control.controller.text = DateFormat(control.format).format(value!);
|
||||||
|
}
|
||||||
|
_temporarilyDisableListeners = false;
|
||||||
|
|
||||||
|
final error = _validateDate(value);
|
||||||
|
setState(() {
|
||||||
|
_error = error;
|
||||||
|
});
|
||||||
|
|
||||||
if (value?.isBefore(widget.firstDate) ?? false) {
|
if (value?.isBefore(widget.firstDate) ?? false) {
|
||||||
return "Date must be before " +
|
setState(() => _error = "Date must be after " +
|
||||||
DateFormat.yMd(widget.locale.toString())
|
DateFormat.yMd(widget.locale.toString())
|
||||||
.format(widget.firstDate);
|
.format(widget.firstDate) +
|
||||||
|
".");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (value?.isAfter(widget.lastDate) ?? false) {
|
if (value?.isAfter(widget.lastDate) ?? false) {
|
||||||
return "Date must be after " +
|
setState(() => _error = "Date must be before " +
|
||||||
DateFormat.yMd(widget.locale.toString())
|
DateFormat.yMd(widget.locale.toString())
|
||||||
.format(widget.lastDate);
|
.format(widget.lastDate) +
|
||||||
|
".");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
name: widget.name,
|
||||||
|
initialValue: widget.initialValue,
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
return SizedBox(
|
return GestureDetector(
|
||||||
height: 56,
|
onTap: () {
|
||||||
|
_textFieldControls.first.node.requestFocus();
|
||||||
|
},
|
||||||
child: InputDecorator(
|
child: InputDecorator(
|
||||||
textAlignVertical: TextAlignVertical.bottom,
|
textAlignVertical: TextAlignVertical.bottom,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
errorText: _error,
|
||||||
labelText: widget.labelText,
|
labelText: widget.labelText,
|
||||||
prefixIcon: widget.prefixIcon,
|
prefixIcon: widget.prefixIcon,
|
||||||
suffixIcon: Row(
|
suffixIcon: Row(
|
||||||
@@ -168,11 +193,11 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
field.didChange(null);
|
|
||||||
for (var c in _textFieldControls) {
|
for (var c in _textFieldControls) {
|
||||||
c.controller.clear();
|
c.controller.clear();
|
||||||
}
|
}
|
||||||
_textFieldControls.first.node.requestFocus();
|
_textFieldControls.first.node.requestFocus();
|
||||||
|
field.didChange(null);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
),
|
),
|
||||||
@@ -182,16 +207,9 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
for (var s in _textFieldControls) ...[
|
for (var s in _textFieldControls) ...[
|
||||||
SizedBox(
|
IntrinsicWidth(
|
||||||
width: switch (s.format) {
|
|
||||||
== "dd" => 32,
|
|
||||||
== "MM" => 32,
|
|
||||||
== "yyyy" => 48,
|
|
||||||
_ => 0,
|
|
||||||
},
|
|
||||||
child: _buildDateSegmentInput(s, context, field),
|
child: _buildDateSegmentInput(s, context, field),
|
||||||
),
|
),
|
||||||
if (s.position < 2) Text(_separator).paddedOnly(right: 4),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -202,6 +220,26 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _validateDate(DateTime? date) {
|
||||||
|
if (widget.allowUnset && date == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (date == null) {
|
||||||
|
return S.of(context)!.thisFieldIsRequired;
|
||||||
|
}
|
||||||
|
if (date.isBefore(widget.firstDate)) {
|
||||||
|
final formattedDateHint =
|
||||||
|
DateFormat.yMd(widget.locale.toString()).format(widget.firstDate);
|
||||||
|
return "Date must be after $formattedDateHint.";
|
||||||
|
}
|
||||||
|
if (date.isAfter(widget.lastDate)) {
|
||||||
|
final formattedDateHint =
|
||||||
|
DateFormat.yMd(widget.locale.toString()).format(widget.lastDate);
|
||||||
|
return "Date must be before $formattedDateHint.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
void _updateInputsWithDate(DateTime date) {
|
void _updateInputsWithDate(DateTime date) {
|
||||||
final components = _format.split(_separator);
|
final components = _format.split(_separator);
|
||||||
for (int i = 0; i < components.length; i++) {
|
for (int i = 0; i < components.length; i++) {
|
||||||
@@ -212,38 +250,69 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDateSegmentInput(
|
Widget _buildDateSegmentInput(
|
||||||
NeighbourAwareDateInputSegmentControls controls,
|
_NeighbourAwareDateInputSegmentControls controls,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
FormFieldState<DateTime> field,
|
FormFieldState<DateTime> field,
|
||||||
) {
|
) {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
onFieldSubmitted: (value) {
|
onFieldSubmitted: (value) {
|
||||||
|
if (value.length < controls.format.length) {
|
||||||
|
controls.controller.text = value.padLeft(controls.format.length, '0');
|
||||||
|
}
|
||||||
_textFieldControls
|
_textFieldControls
|
||||||
.elementAt(controls.position)
|
.elementAt(controls.position)
|
||||||
.next
|
.next
|
||||||
?.node
|
?.node
|
||||||
.requestFocus();
|
.requestFocus();
|
||||||
},
|
},
|
||||||
// onTap: () {
|
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||||
// controls.controller.clear();
|
|
||||||
// },
|
|
||||||
canRequestFocus: true,
|
|
||||||
keyboardType: TextInputType.datetime,
|
keyboardType: TextInputType.datetime,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction:
|
||||||
|
controls.position < 2 ? TextInputAction.next : TextInputAction.done,
|
||||||
controller: controls.controller,
|
controller: controls.controller,
|
||||||
focusNode: _textFieldControls.elementAt(controls.position).node,
|
focusNode: _textFieldControls.elementAt(controls.position).node,
|
||||||
maxLength: controls.format.length,
|
maxLength: controls.format.length,
|
||||||
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
|
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
||||||
enableInteractiveSelection: false,
|
enableInteractiveSelection: false,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.length == controls.format.length && field.value != null) {
|
||||||
|
final number = int.tryParse(value);
|
||||||
|
if (number == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final newValue = switch (controls.type) {
|
||||||
|
_DateInputSegment.day => field.value!.copyWith(day: number),
|
||||||
|
_DateInputSegment.month => field.value!.copyWith(month: number),
|
||||||
|
_DateInputSegment.year => field.value!.copyWith(year: number),
|
||||||
|
};
|
||||||
|
field.didChange(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
ReplacingTextFormatter(),
|
RangeLimitedInputFormatter(
|
||||||
|
1,
|
||||||
|
switch (controls.type) {
|
||||||
|
_DateInputSegment.day => 31,
|
||||||
|
_DateInputSegment.month => 12,
|
||||||
|
_DateInputSegment.year => 9999,
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
suffixIcon: controls.position < 2
|
||||||
|
? Text(
|
||||||
|
_separator,
|
||||||
|
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||||
|
).paddedSymmetrically(horizontal: 2)
|
||||||
|
: null,
|
||||||
|
suffixIconConstraints: const BoxConstraints.tightFor(),
|
||||||
|
fillColor: Colors.blue.values[controls.position],
|
||||||
counterText: '',
|
counterText: '',
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
hintText: controls.format,
|
hintText: controls.format,
|
||||||
|
hintStyle: const TextStyle(fontFamily: "RobotoMono"),
|
||||||
border: Theme.of(context).inputDecorationTheme.border?.copyWith(
|
border: Theme.of(context).inputDecorationTheme.border?.copyWith(
|
||||||
borderSide: const BorderSide(
|
borderSide: const BorderSide(
|
||||||
width: 0,
|
width: 0,
|
||||||
@@ -255,31 +324,64 @@ class _FormBuilderLocalizedDatePickerState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReplacingTextFormatter extends TextInputFormatter {
|
enum _DateInputSegment {
|
||||||
|
day,
|
||||||
|
month,
|
||||||
|
year;
|
||||||
|
|
||||||
|
static _DateInputSegment fromPattern(String pattern) {
|
||||||
|
final char = pattern.characters.first;
|
||||||
|
return switch (char) {
|
||||||
|
'd' => day,
|
||||||
|
'M' => month,
|
||||||
|
'y' => year,
|
||||||
|
_ => throw ArgumentError.value(pattern),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _NeighbourAwareDateInputSegmentControls
|
||||||
|
with LinkedListEntry<_NeighbourAwareDateInputSegmentControls> {
|
||||||
|
final FocusNode node;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final int position;
|
||||||
|
final String format;
|
||||||
|
final _DateInputSegment type;
|
||||||
|
|
||||||
|
_NeighbourAwareDateInputSegmentControls({
|
||||||
|
required this.node,
|
||||||
|
required this.controller,
|
||||||
|
required this.format,
|
||||||
|
required this.position,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class RangeLimitedInputFormatter extends TextInputFormatter {
|
||||||
|
RangeLimitedInputFormatter(
|
||||||
|
this.minimum,
|
||||||
|
this.maximum,
|
||||||
|
) : assert(minimum < maximum);
|
||||||
|
|
||||||
|
final int minimum;
|
||||||
|
final int maximum;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextEditingValue formatEditUpdate(
|
TextEditingValue formatEditUpdate(
|
||||||
TextEditingValue oldValue,
|
TextEditingValue oldValue,
|
||||||
TextEditingValue newValue,
|
TextEditingValue newValue,
|
||||||
) {
|
) {
|
||||||
final oldOffset = oldValue.selection.baseOffset;
|
if (newValue.text.length < 2) {
|
||||||
final newOffset = newValue.selection.baseOffset;
|
return newValue;
|
||||||
final replacement = newValue.text.substring(oldOffset, newOffset);
|
}
|
||||||
print(
|
var value = int.parse(newValue.text);
|
||||||
"DBG: Received ${oldValue.text} -> ${newValue.text}. New char = $replacement");
|
final lastCharacter = newValue.text.characters.last;
|
||||||
if (oldOffset < newOffset) {
|
if (value < minimum || value > maximum) {
|
||||||
final oldText = oldValue.text;
|
return TextEditingValue(
|
||||||
final newText = oldText.replaceRange(
|
text: lastCharacter,
|
||||||
oldOffset,
|
selection: TextSelection.collapsed(offset: 1),
|
||||||
newOffset,
|
|
||||||
newValue.text.substring(oldOffset, newOffset),
|
|
||||||
);
|
|
||||||
print("DBG: Replacing $oldText -> $newText");
|
|
||||||
return newValue.copyWith(
|
|
||||||
text: newText,
|
|
||||||
selection: TextSelection.collapsed(offset: newOffset),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newValue;
|
return newValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class AppDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
'https://github.com/astubenbord/paperless-mobile/issues/new',
|
'https://github.com/astubenbord/paperless-mobile/issues/new?assignees=astubenbord&labels=bug%2Ctriage&projects=&template=bug-report.yml&title=%5BBug%5D%3A+',
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -413,28 +413,16 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
|
|
||||||
Widget _buildCreatedAtFormField(
|
Widget _buildCreatedAtFormField(
|
||||||
DateTime? initialCreatedAtDate, FieldSuggestions? filteredSuggestions) {
|
DateTime? initialCreatedAtDate, FieldSuggestions? filteredSuggestions) {
|
||||||
// return FormBuilderLocalizedDatePicker(
|
|
||||||
// name: fkCreatedDate,
|
|
||||||
// initialValue: initialCreatedAtDate,
|
|
||||||
// labelText: S.of(context)!.createdAt,
|
|
||||||
// firstDate: DateTime(1970, 1, 1),
|
|
||||||
// lastDate: DateTime.now(),
|
|
||||||
// locale: Localizations.localeOf(context),
|
|
||||||
// prefixIcon: Icon(Icons.calendar_today),
|
|
||||||
// );
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
FormBuilderDateTimePicker(
|
FormBuilderLocalizedDatePicker(
|
||||||
inputType: InputType.date,
|
|
||||||
name: fkCreatedDate,
|
name: fkCreatedDate,
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
|
||||||
label: Text(S.of(context)!.createdAt),
|
|
||||||
),
|
|
||||||
initialValue: initialCreatedAtDate,
|
initialValue: initialCreatedAtDate,
|
||||||
format: DateFormat.yMMMMd(Localizations.localeOf(context).toString()),
|
labelText: S.of(context)!.createdAt,
|
||||||
initialEntryMode: DatePickerEntryMode.calendar,
|
firstDate: DateTime(1970, 1, 1),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
locale: Localizations.localeOf(context),
|
||||||
|
prefixIcon: Icon(Icons.calendar_today),
|
||||||
),
|
),
|
||||||
if (filteredSuggestions?.hasSuggestedDates ?? false)
|
if (filteredSuggestions?.hasSuggestedDates ?? false)
|
||||||
_buildSuggestionsSkeleton<DateTime>(
|
_buildSuggestionsSkeleton<DateTime>(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
class LocalDateTimeJsonConverter extends JsonConverter<DateTime, String> {
|
class LocalDateTimeJsonConverter extends JsonConverter<DateTime, String> {
|
||||||
@@ -11,6 +10,6 @@ class LocalDateTimeJsonConverter extends JsonConverter<DateTime, String> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toJson(DateTime object) {
|
String toJson(DateTime object) {
|
||||||
return object.toIso8601String();
|
return object.toUtc().toIso8601String();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
pubspec.yaml
30
pubspec.yaml
@@ -155,33 +155,15 @@ flutter:
|
|||||||
- test/fixtures/document_types/
|
- test/fixtures/document_types/
|
||||||
- assets/changelogs/
|
- assets/changelogs/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
fonts:
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
- family: RobotoMono
|
||||||
# For details regarding adding assets from package dependencies, see
|
fonts:
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
- asset: assets/fonts/RobotoMono-Regular.ttf
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
image: assets/logos/paperless_logo_green.png
|
image: assets/logos/paperless_logo_green.png
|
||||||
color: "#f9f9f9"
|
color: "#f9f9f9"
|
||||||
|
|
||||||
image_dark: assets/logos/paperless_logo_white.png
|
image_dark: assets/logos/paperless_logo_white.png
|
||||||
color_dark: "#181818"
|
color_dark: "#181818"
|
||||||
|
|||||||
Reference in New Issue
Block a user