feat: initial date text input

This commit is contained in:
Mathias Mogensen 2024-03-05 13:18:48 +01:00
parent 5daf9d23f5
commit 6a14ba8047
3 changed files with 302 additions and 18 deletions

View File

@ -1,3 +1,4 @@
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -7,6 +8,28 @@ const _isoFmt = 'y-M-d';
const _friendlyFmt = 'MMM d, y'; const _friendlyFmt = 'MMM d, y';
const _dmyFmt = 'd/M/y'; const _dmyFmt = 'd/M/y';
extension FormatDate on DateFormatPB {
String formatDate(
DateTime date,
bool includeTime, [
UserTimeFormatPB? timeFormat,
]) {
final global = _toGlobalFormat();
return global.formatDate(date, includeTime, timeFormat);
}
UserDateFormatPB _toGlobalFormat() {
return switch (this) {
DateFormatPB.DayMonthYear => UserDateFormatPB.DayMonthYear,
DateFormatPB.Friendly => UserDateFormatPB.Friendly,
DateFormatPB.ISO => UserDateFormatPB.ISO,
DateFormatPB.Local => UserDateFormatPB.Locally,
DateFormatPB.US => UserDateFormatPB.US,
_ => UserDateFormatPB.Friendly,
};
}
}
extension DateFormatter on UserDateFormatPB { extension DateFormatter on UserDateFormatPB {
DateFormat get toFormat => DateFormat(_toFormat[this] ?? _friendlyFmt); DateFormat get toFormat => DateFormat(_toFormat[this] ?? _friendlyFmt);

View File

@ -3,10 +3,9 @@ import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_time_input.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
@ -179,24 +178,61 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
StartTextField( // Start Date and Time input
includeTime: widget.includeTime, DateTimeInput(
timeFormat: widget.timeFormat,
timeHintText: widget.timeHintText,
parseEndTimeError: widget.parseEndTimeError,
parseTimeError: widget.parseTimeError,
timeStr: widget.timeStr,
popoverMutex: widget.popoverMutex, popoverMutex: widget.popoverMutex,
onSubmitted: widget.onStartTimeSubmitted, isTimeEnabled: widget.includeTime,
), dateOptions: DateOptions(
EndTextField( dateFormat: widget.dateFormat,
includeTime: widget.includeTime, date: widget.startDay ?? widget.selectedDay,
timeFormat: widget.timeFormat, ),
isRange: widget.isRange, timeOptions: widget.includeTime
endTimeStr: widget.endTimeStr, ? TimeOptions(
popoverMutex: widget.popoverMutex, timeFormat: widget.timeFormat,
onSubmitted: widget.onEndTimeSubmitted, timeStr: widget.timeStr,
parseTimeError: widget.parseTimeError,
onSubmitted: widget.onStartTimeSubmitted,
)
: null,
), ),
// End Date and Time input
if (widget.isRange) ...[
const VSpace(6),
DateTimeInput(
popoverMutex: widget.popoverMutex,
isTimeEnabled: widget.includeTime,
dateOptions: DateOptions(
dateFormat: widget.dateFormat,
date: widget.endDay,
),
timeOptions: widget.includeTime
? TimeOptions(
timeFormat: widget.timeFormat,
timeStr: widget.endTimeStr,
parseTimeError: widget.parseEndTimeError,
onSubmitted: widget.onEndTimeSubmitted,
)
: null,
),
],
// StartTextField(
// includeTime: widget.includeTime,
// timeFormat: widget.timeFormat,
// timeHintText: widget.timeHintText,
// parseEndTimeError: widget.parseEndTimeError,
// parseTimeError: widget.parseTimeError,
// timeStr: widget.timeStr,
// popoverMutex: widget.popoverMutex,
// onSubmitted: widget.onStartTimeSubmitted,
// ),
// EndTextField(
// includeTime: widget.includeTime,
// timeFormat: widget.timeFormat,
// isRange: widget.isRange,
// endTimeStr: widget.endTimeStr,
// popoverMutex: widget.popoverMutex,
// onSubmitted: widget.onEndTimeSubmitted,
// ),
DatePicker( DatePicker(
isRange: widget.isRange, isRange: widget.isRange,
onDaySelected: (selectedDay, focusedDay) { onDaySelected: (selectedDay, focusedDay) {

View File

@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
class TimeOptions {
const TimeOptions({
required this.timeFormat,
this.timeHintText,
this.parseTimeError,
this.timeStr,
this.onSubmitted,
});
final TimeFormatPB timeFormat;
final String? timeHintText;
final String? parseTimeError;
final String? timeStr;
final Function(String timeStr)? onSubmitted;
}
class DateOptions {
DateOptions({
required this.dateFormat,
required this.date,
});
final DateFormatPB dateFormat;
final DateTime? date;
}
class DateTimeInput extends StatelessWidget {
const DateTimeInput({
super.key,
this.popoverMutex,
this.isTimeEnabled = true,
required this.dateOptions,
this.timeOptions,
}) : assert(!isTimeEnabled || timeOptions != null);
final PopoverMutex? popoverMutex;
final bool isTimeEnabled;
final DateOptions dateOptions;
final TimeOptions? timeOptions;
@override
Widget build(BuildContext context) {
final inputDecoration = _defaultInputDecoration(context);
final leftBorder = _inputBorderFromSide(context, true);
final rightBorder = _inputBorderFromSide(context, false);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: Corners.s8Border,
),
child: Row(
children: [
Flexible(
child: _DatePart(
inputDecoration: isTimeEnabled
? inputDecoration.copyWith(
enabledBorder: leftBorder,
focusedBorder: leftBorder,
)
: inputDecoration,
options: dateOptions,
),
),
if (isTimeEnabled && timeOptions != null) ...[
SizedBox(
height: 18,
child: VerticalDivider(
width: 4,
color: Theme.of(context).colorScheme.outline,
),
),
Flexible(
child: _TimePart(
inputDecoration: isTimeEnabled
? inputDecoration.copyWith(
enabledBorder: rightBorder,
focusedBorder: rightBorder,
)
: inputDecoration,
options: timeOptions!,
),
),
],
],
),
),
if (timeOptions?.parseTimeError?.isNotEmpty ?? false) ...[
const VSpace(4),
Text(
timeOptions!.parseTimeError!,
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Theme.of(context).colorScheme.error),
),
],
],
),
);
}
InputBorder _inputBorderFromSide(BuildContext context, bool isLeft) =>
OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.only(
topLeft: isLeft ? Corners.s8Radius : Radius.zero,
bottomLeft: isLeft ? Corners.s8Radius : Radius.zero,
topRight: !isLeft ? Corners.s8Radius : Radius.zero,
bottomRight: !isLeft ? Corners.s8Radius : Radius.zero,
),
);
InputDecoration _defaultInputDecoration(BuildContext context) =>
InputDecoration(
constraints: const BoxConstraints(maxHeight: 32),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
borderRadius: Corners.s8Border,
),
isDense: false,
errorStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Theme.of(context).colorScheme.error),
hintStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Theme.of(context).hintColor),
suffixText: "",
counterText: "",
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
borderRadius: Corners.s8Border,
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.error),
borderRadius: Corners.s8Border,
),
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.error),
borderRadius: Corners.s8Border,
),
);
}
class _DatePart extends StatefulWidget {
const _DatePart({
required this.inputDecoration,
required this.options,
});
final InputDecoration inputDecoration;
final DateOptions options;
@override
State<_DatePart> createState() => _DatePartState();
}
class _DatePartState extends State<_DatePart> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
final dateStr = widget.options.date != null
? widget.options.dateFormat.formatDate(
widget.options.date!,
false,
)
: "";
_controller = TextEditingController(text: dateStr);
}
@override
Widget build(BuildContext context) {
return FlowyTextField(
controller: _controller,
decoration: widget.inputDecoration,
);
}
}
class _TimePart extends StatefulWidget {
const _TimePart({
required this.inputDecoration,
required this.options,
});
final InputDecoration inputDecoration;
final TimeOptions options;
@override
State<_TimePart> createState() => _TimePartState();
}
class _TimePartState extends State<_TimePart> {
late final _controller = TextEditingController(text: widget.options.timeStr);
@override
Widget build(BuildContext context) {
return FlowyTextField(
controller: _controller,
decoration: widget.inputDecoration,
onSubmitted: widget.options.onSubmitted,
);
}
}