diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 5dad0c98cc..43633f9780 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -258,11 +258,11 @@ class _MentionDateBlockState extends State { } DateTime _parseTime(String timeStr, UserTimeFormatPB timeFormat) { - final twelveHourFormat = DateFormat('HH:mm a'); + final twelveHourFormat = DateFormat('hh:mm a'); final twentyFourHourFormat = DateFormat('HH:mm'); try { - if (timeFormat == TimeFormatPB.TwelveHour) { + if (timeFormat == UserTimeFormatPB.TwelveHour) { return twelveHourFormat.parseStrict(timeStr); } 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 c68fc76ea6..9fa92446b4 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 @@ -174,85 +174,92 @@ class _AppFlowyDatePickerState extends State { } Widget buildDesktopPicker() { - return Padding( - padding: const EdgeInsets.only(top: 18.0, bottom: 12.0), - 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, - 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) { - widget.onDaySelected?.call(selectedDay, focusedDay); - - if (widget.rebuildOnDaySelected) { - setState(() => _selectedDay = selectedDay); - } - }, - onRangeSelected: widget.onRangeSelected, - selectedDay: - widget.rebuildOnDaySelected ? _selectedDay : widget.selectedDay, - firstDay: widget.firstDay, - lastDay: widget.lastDay, - startDay: widget.startDay, - endDay: widget.endDay, - onCalendarCreated: widget.onCalendarCreated, - onPageChanged: widget.onPageChanged, - ), - const TypeOptionSeparator(spacing: 12.0), - if (widget.enableRanges && widget.onIsRangeChanged != null) ...[ - EndTimeButton( + // GestureDetector is a workaround to stop popover from closing + // when clicking on the date picker. + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Padding( + padding: const EdgeInsets.only(top: 18.0, bottom: 12.0), + 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, + popoverMutex: widget.popoverMutex, + onSubmitted: widget.onStartTimeSubmitted, + ), + EndTextField( + includeTime: widget.includeTime, + timeFormat: widget.timeFormat, isRange: widget.isRange, - onChanged: widget.onIsRangeChanged!, + endTimeStr: widget.endTimeStr, + popoverMutex: widget.popoverMutex, + onSubmitted: widget.onEndTimeSubmitted, ), - const VSpace(4.0), - ], - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: IncludeTimeButton( - value: widget.includeTime, - onChanged: widget.onIncludeTimeChanged, + DatePicker( + isRange: widget.isRange, + onDaySelected: (selectedDay, focusedDay) { + widget.onDaySelected?.call(selectedDay, focusedDay); + + if (widget.rebuildOnDaySelected) { + setState(() => _selectedDay = selectedDay); + } + }, + onRangeSelected: widget.onRangeSelected, + selectedDay: widget.rebuildOnDaySelected + ? _selectedDay + : widget.selectedDay, + firstDay: widget.firstDay, + lastDay: widget.lastDay, + startDay: widget.startDay, + endDay: widget.endDay, + onCalendarCreated: widget.onCalendarCreated, + onPageChanged: widget.onPageChanged, + ), + const TypeOptionSeparator(spacing: 12.0), + if (widget.enableRanges && widget.onIsRangeChanged != null) ...[ + EndTimeButton( + isRange: widget.isRange, + onChanged: widget.onIsRangeChanged!, + ), + const VSpace(4.0), + ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: IncludeTimeButton( + value: widget.includeTime, + onChanged: widget.onIncludeTimeChanged, + ), ), - ), - const _GroupSeparator(), - ReminderSelector( - mutex: widget.popoverMutex, - hasTime: widget.includeTime, - timeFormat: widget.timeFormat, - selectedOption: _selectedReminderOption, - onOptionSelected: (option) { - setState(() => _selectedReminderOption = option); - widget.onReminderSelected?.call(option); - }, - ), - if (widget.options?.isNotEmpty ?? false) ...[ const _GroupSeparator(), - ListView.separated( - shrinkWrap: true, - itemCount: widget.options!.length, - separatorBuilder: (_, __) => const _GroupSeparator(), - itemBuilder: (_, index) => - _renderGroupOptions(widget.options![index].options), + ReminderSelector( + mutex: widget.popoverMutex, + hasTime: widget.includeTime, + timeFormat: widget.timeFormat, + selectedOption: _selectedReminderOption, + onOptionSelected: (option) { + setState(() => _selectedReminderOption = option); + widget.onReminderSelected?.call(option); + }, ), + if (widget.options?.isNotEmpty ?? false) ...[ + const _GroupSeparator(), + ListView.separated( + shrinkWrap: true, + itemCount: widget.options!.length, + separatorBuilder: (_, __) => const _GroupSeparator(), + itemBuilder: (_, index) => + _renderGroupOptions(widget.options![index].options), + ), + ], ], - ], + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart index 46c2e08a31..c0f5cc8308 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:intl/intl.dart'; const _maxLengthTwelveHour = 8; const _maxLengthTwentyFourHour = 5; @@ -55,6 +56,13 @@ class _TimeTextFieldState extends State { text = widget.endTimeStr!; } + if (widget.timeFormat == TimeFormatPB.TwelveHour) { + final twentyFourHourFormat = DateFormat('HH:mm'); + final twelveHourFormat = DateFormat('hh:mm a'); + final date = twentyFourHourFormat.parse(text); + text = twelveHourFormat.format(date); + } + _focusNode.addListener(_focusNodeListener); widget.popoverMutex?.listenOnPopoverChanged(_popoverListener); } @@ -86,6 +94,7 @@ class _TimeTextFieldState extends State { padding: const EdgeInsets.symmetric(horizontal: 18.0), child: FlowyTextField( text: text, + keyboardType: TextInputType.datetime, focusNode: _focusNode, autoFocus: false, controller: _textController, @@ -97,7 +106,16 @@ class _TimeTextFieldState extends State { ? _maxLengthTwelveHour : _maxLengthTwentyFourHour, showCounter: false, - inputFormatters: [TimeInputFormatter(widget.timeFormat)], + inputFormatters: [ + if (widget.timeFormat == TimeFormatPB.TwelveHour) ...[ + // Allow for AM/PM if time format is 12-hour + FilteringTextInputFormatter.allow(RegExp('[0-9:aApPmM ]')), + ] else ...[ + // Default allow for hh:mm format + FilteringTextInputFormatter.allow(RegExp('[0-9:]')), + ], + TimeInputFormatter(widget.timeFormat), + ], onSubmitted: widget.onSubmitted, ), ); @@ -134,7 +152,20 @@ class TimeInputFormatter extends TextInputFormatter { return _formatText(newText, spacePosition, ' '); } - return newValue; + if (timeFormat == TimeFormatPB.TwentyFourHour && + newValue.text.length == 5) { + final prefix = newValue.text.substring(0, 3); + final suffix = newValue.text.length > 5 ? newValue.text.substring(6) : ''; + + final minutes = int.tryParse(newValue.text.substring(3, 5)); + if (minutes == null || minutes <= 0) { + return newValue.copyWith(text: '${prefix}00$suffix'.toUpperCase()); + } else if (minutes > 59) { + return newValue.copyWith(text: '${prefix}59$suffix'.toUpperCase()); + } + } + + return newValue.copyWith(text: newText.toUpperCase()); } TextEditingValue _formatText(String text, int index, String separator) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 1fce5c0714..e9aab3bfb6 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flowy_infra/size.dart'; + class FlowyTextField extends StatefulWidget { final String? hintText; final String? text; @@ -31,6 +32,7 @@ class FlowyTextField extends StatefulWidget { final TextStyle? hintStyle; final InputDecoration? decoration; final TextAlignVertical? textAlignVertical; + final TextInputAction? textInputAction; final TextInputType? keyboardType; final List? inputFormatters; @@ -62,6 +64,7 @@ class FlowyTextField extends StatefulWidget { this.hintStyle, this.decoration, this.textAlignVertical, + this.textInputAction, this.keyboardType = TextInputType.multiline, this.inputFormatters, });