fix: twelve hour parsing issue (#4721)

* fix: twelve hour time format fixed

* fix: time format for 12hour

* feat: auto capitalize am-pm in formatter

* fix: time field placeholder based on user time format

* chore: bugfix + improvements

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
This commit is contained in:
Ansah Mohammad 2024-04-08 07:03:18 +05:30 committed by GitHub
parent 9536cde789
commit 47d321b8c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 119 additions and 78 deletions

View File

@ -258,11 +258,11 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
} }
DateTime _parseTime(String timeStr, UserTimeFormatPB timeFormat) { DateTime _parseTime(String timeStr, UserTimeFormatPB timeFormat) {
final twelveHourFormat = DateFormat('HH:mm a'); final twelveHourFormat = DateFormat('hh:mm a');
final twentyFourHourFormat = DateFormat('HH:mm'); final twentyFourHourFormat = DateFormat('HH:mm');
try { try {
if (timeFormat == TimeFormatPB.TwelveHour) { if (timeFormat == UserTimeFormatPB.TwelveHour) {
return twelveHourFormat.parseStrict(timeStr); return twelveHourFormat.parseStrict(timeStr);
} }

View File

@ -174,85 +174,92 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
} }
Widget buildDesktopPicker() { Widget buildDesktopPicker() {
return Padding( // GestureDetector is a workaround to stop popover from closing
padding: const EdgeInsets.only(top: 18.0, bottom: 12.0), // when clicking on the date picker.
child: Column( return GestureDetector(
mainAxisSize: MainAxisSize.min, behavior: HitTestBehavior.opaque,
children: [ onTap: () {},
StartTextField( child: Padding(
includeTime: widget.includeTime, padding: const EdgeInsets.only(top: 18.0, bottom: 12.0),
timeFormat: widget.timeFormat, child: Column(
timeHintText: widget.timeHintText, mainAxisSize: MainAxisSize.min,
parseEndTimeError: widget.parseEndTimeError, children: [
parseTimeError: widget.parseTimeError, StartTextField(
timeStr: widget.timeStr, includeTime: widget.includeTime,
popoverMutex: widget.popoverMutex, timeFormat: widget.timeFormat,
onSubmitted: widget.onStartTimeSubmitted, timeHintText: widget.timeHintText,
), parseEndTimeError: widget.parseEndTimeError,
EndTextField( parseTimeError: widget.parseTimeError,
includeTime: widget.includeTime, timeStr: widget.timeStr,
timeFormat: widget.timeFormat, popoverMutex: widget.popoverMutex,
isRange: widget.isRange, onSubmitted: widget.onStartTimeSubmitted,
endTimeStr: widget.endTimeStr, ),
popoverMutex: widget.popoverMutex, EndTextField(
onSubmitted: widget.onEndTimeSubmitted, includeTime: widget.includeTime,
), timeFormat: widget.timeFormat,
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, isRange: widget.isRange,
onChanged: widget.onIsRangeChanged!, endTimeStr: widget.endTimeStr,
popoverMutex: widget.popoverMutex,
onSubmitted: widget.onEndTimeSubmitted,
), ),
const VSpace(4.0), DatePicker(
], isRange: widget.isRange,
Padding( onDaySelected: (selectedDay, focusedDay) {
padding: const EdgeInsets.symmetric(horizontal: 12.0), widget.onDaySelected?.call(selectedDay, focusedDay);
child: IncludeTimeButton(
value: widget.includeTime, if (widget.rebuildOnDaySelected) {
onChanged: widget.onIncludeTimeChanged, 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(), const _GroupSeparator(),
ListView.separated( ReminderSelector(
shrinkWrap: true, mutex: widget.popoverMutex,
itemCount: widget.options!.length, hasTime: widget.includeTime,
separatorBuilder: (_, __) => const _GroupSeparator(), timeFormat: widget.timeFormat,
itemBuilder: (_, index) => selectedOption: _selectedReminderOption,
_renderGroupOptions(widget.options![index].options), 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),
),
],
], ],
], ),
), ),
); );
} }

View File

@ -4,6 +4,7 @@ import 'package:flutter/services.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_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:intl/intl.dart';
const _maxLengthTwelveHour = 8; const _maxLengthTwelveHour = 8;
const _maxLengthTwentyFourHour = 5; const _maxLengthTwentyFourHour = 5;
@ -55,6 +56,13 @@ class _TimeTextFieldState extends State<TimeTextField> {
text = widget.endTimeStr!; 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); _focusNode.addListener(_focusNodeListener);
widget.popoverMutex?.listenOnPopoverChanged(_popoverListener); widget.popoverMutex?.listenOnPopoverChanged(_popoverListener);
} }
@ -86,6 +94,7 @@ class _TimeTextFieldState extends State<TimeTextField> {
padding: const EdgeInsets.symmetric(horizontal: 18.0), padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: FlowyTextField( child: FlowyTextField(
text: text, text: text,
keyboardType: TextInputType.datetime,
focusNode: _focusNode, focusNode: _focusNode,
autoFocus: false, autoFocus: false,
controller: _textController, controller: _textController,
@ -97,7 +106,16 @@ class _TimeTextFieldState extends State<TimeTextField> {
? _maxLengthTwelveHour ? _maxLengthTwelveHour
: _maxLengthTwentyFourHour, : _maxLengthTwentyFourHour,
showCounter: false, 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, onSubmitted: widget.onSubmitted,
), ),
); );
@ -134,7 +152,20 @@ class TimeInputFormatter extends TextInputFormatter {
return _formatText(newText, spacePosition, ' '); 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) { TextEditingValue _formatText(String text, int index, String separator) {

View File

@ -1,9 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flowy_infra/size.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flowy_infra/size.dart';
class FlowyTextField extends StatefulWidget { class FlowyTextField extends StatefulWidget {
final String? hintText; final String? hintText;
final String? text; final String? text;
@ -31,6 +32,7 @@ class FlowyTextField extends StatefulWidget {
final TextStyle? hintStyle; final TextStyle? hintStyle;
final InputDecoration? decoration; final InputDecoration? decoration;
final TextAlignVertical? textAlignVertical; final TextAlignVertical? textAlignVertical;
final TextInputAction? textInputAction;
final TextInputType? keyboardType; final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters; final List<TextInputFormatter>? inputFormatters;
@ -62,6 +64,7 @@ class FlowyTextField extends StatefulWidget {
this.hintStyle, this.hintStyle,
this.decoration, this.decoration,
this.textAlignVertical, this.textAlignVertical,
this.textInputAction,
this.keyboardType = TextInputType.multiline, this.keyboardType = TextInputType.multiline,
this.inputFormatters, this.inputFormatters,
}); });