From 6a14ba804700c148681edd4e11cf7654b025391e Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 5 Mar 2024 13:18:48 +0100 Subject: [PATCH] feat: initial date text input --- .../settings/date_time/date_format_ext.dart | 23 ++ .../date_picker/appflowy_date_picker.dart | 72 ++++-- .../date_picker/widgets/date_time_input.dart | 225 ++++++++++++++++++ 3 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_input.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart index 863482cca8..b7f2e70ab7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart @@ -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:easy_localization/easy_localization.dart'; @@ -7,6 +8,28 @@ const _isoFmt = 'y-M-d'; const _friendlyFmt = 'MMM d, 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 { DateFormat get toFormat => DateFormat(_toFormat[this] ?? _friendlyFmt); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart index 93ab2b2754..05aedab569 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart @@ -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/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/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/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_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -179,24 +178,61 @@ class _AppFlowyDatePickerState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - StartTextField( - includeTime: widget.includeTime, - timeFormat: widget.timeFormat, - timeHintText: widget.timeHintText, - parseEndTimeError: widget.parseEndTimeError, - parseTimeError: widget.parseTimeError, - timeStr: widget.timeStr, + // Start Date and Time input + DateTimeInput( 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, + isTimeEnabled: widget.includeTime, + dateOptions: DateOptions( + dateFormat: widget.dateFormat, + date: widget.startDay ?? widget.selectedDay, + ), + timeOptions: widget.includeTime + ? TimeOptions( + timeFormat: widget.timeFormat, + 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( isRange: widget.isRange, onDaySelected: (selectedDay, focusedDay) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_input.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_input.dart new file mode 100644 index 0000000000..19219ed599 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_input.dart @@ -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, + ); + } +}