diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 41f68c1076..7b67d4f084 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -1,5 +1,9 @@ import 'dart:io'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; @@ -26,14 +30,12 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart'; -import 'package:appflowy/plugins/database_view/widgets/setting/database_layout_selector.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart'; import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_header.dart'; @@ -51,10 +53,15 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_layout_selector.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/database_setting_action.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/database_settings_list.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; @@ -67,9 +74,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:table_calendar/table_calendar.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart index f312508317..514113abda 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart @@ -1,25 +1,20 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'builder.dart'; import 'date.dart'; class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { - final TimestampTypeOptionWidget _widget; - TimestampTypeOptionWidgetBuilder( TimestampTypeOptionContext typeOptionContext, PopoverMutex popoverMutex, @@ -28,21 +23,22 @@ class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { popoverMutex: popoverMutex, ); + final TimestampTypeOptionWidget _widget; + @override - Widget? build(BuildContext context) { - return _widget; - } + Widget? build(BuildContext context) => _widget; } class TimestampTypeOptionWidget extends TypeOptionWidget { - final TimestampTypeOptionContext typeOptionContext; - final PopoverMutex popoverMutex; const TimestampTypeOptionWidget({ + super.key, required this.typeOptionContext, required this.popoverMutex, - super.key, }); + final TimestampTypeOptionContext typeOptionContext; + final PopoverMutex popoverMutex; + @override Widget build(BuildContext context) { return BlocProvider( @@ -71,7 +67,7 @@ class TimestampTypeOptionWidget extends TypeOptionWidget { shrinkWrap: true, separatorBuilder: (context, index) { if (index == 0) { - return const SizedBox(); + return const SizedBox.shrink(); } else { return VSpace(GridSize.typeOptionSeparatorHeight); } @@ -94,17 +90,15 @@ class TimestampTypeOptionWidget extends TypeOptionWidget { triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(8, 0), constraints: BoxConstraints.loose(const Size(460, 440)), - popupBuilder: (popoverContext) { - return DateFormatList( - selectedFormat: dataFormat, - onSelected: (format) { - context - .read() - .add(TimestampTypeOptionEvent.didSelectDateFormat(format)); - PopoverContainer.of(popoverContext).close(); - }, - ); - }, + popupBuilder: (popoverContext) => DateFormatList( + selectedFormat: dataFormat, + onSelected: (format) { + context + .read() + .add(TimestampTypeOptionEvent.didSelectDateFormat(format)); + PopoverContainer.of(popoverContext).close(); + }, + ), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 12.0), child: DateFormatButton(), @@ -122,17 +116,15 @@ class TimestampTypeOptionWidget extends TypeOptionWidget { triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(8, 0), constraints: BoxConstraints.loose(const Size(460, 440)), - popupBuilder: (BuildContext popoverContext) { - return TimeFormatList( - selectedFormat: timeFormat, - onSelected: (format) { - context - .read() - .add(TimestampTypeOptionEvent.didSelectTimeFormat(format)); - PopoverContainer.of(popoverContext).close(); - }, - ); - }, + popupBuilder: (BuildContext popoverContext) => TimeFormatList( + selectedFormat: timeFormat, + onSelected: (format) { + context + .read() + .add(TimestampTypeOptionEvent.didSelectTimeFormat(format)); + PopoverContainer.of(popoverContext).close(); + }, + ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: TimeFormatButton(timeFormat: timeFormat), @@ -140,40 +132,3 @@ class TimestampTypeOptionWidget extends TypeOptionWidget { ); } } - -class IncludeTimeButton extends StatelessWidget { - final bool value; - final Function(bool value) onChanged; - const IncludeTimeButton({ - super.key, - required this.onChanged, - required this.value, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: GridSize.typeOptionContentInsets, - child: Row( - children: [ - FlowySvg( - FlowySvgs.clock_alarm_s, - color: Theme.of(context).iconTheme.color, - ), - const HSpace(6), - FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()), - const Spacer(), - Toggle( - value: value, - onChanged: onChanged, - style: ToggleStyle.big, - padding: EdgeInsets.zero, - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart index 1264dee5a5..0b81e78a34 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart @@ -1,38 +1,27 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_calendar.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:dartz/dartz.dart' show Either; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:table_calendar/table_calendar.dart'; -import '../../../../grid/presentation/layout/sizes.dart'; -import '../../../../grid/presentation/widgets/common/type_option_separator.dart'; -import '../../../../grid/presentation/widgets/header/type_option/date.dart'; import 'date_cal_bloc.dart'; class DateCellEditor extends StatefulWidget { - final VoidCallback onDismissed; - final DateCellController cellController; - const DateCellEditor({ super.key, required this.onDismissed, required this.cellController, }); + final VoidCallback onDismissed; + final DateCellController cellController; + @override State createState() => _DateCellEditor(); } @@ -41,10 +30,8 @@ class _DateCellEditor extends State { @override Widget build(BuildContext context) { return FutureBuilder>( - future: widget.cellController.getTypeOption( - DateTypeOptionDataParser(), - ), - builder: (BuildContext context, snapshot) { + future: widget.cellController.getTypeOption(DateTypeOptionDataParser()), + builder: (_, snapshot) { if (snapshot.hasData) { return _buildWidget(snapshot); } @@ -56,12 +43,10 @@ class _DateCellEditor extends State { Widget _buildWidget(AsyncSnapshot> snapshot) { return snapshot.data!.fold( - (dateTypeOptionPB) { - return _CellCalendarWidget( - cellContext: widget.cellController, - dateTypeOptionPB: dateTypeOptionPB, - ); - }, + (dateTypeOptionPB) => _CellCalendarWidget( + cellContext: widget.cellController, + dateTypeOptionPB: dateTypeOptionPB, + ), (err) { Log.error(err); return const SizedBox.shrink(); @@ -86,6 +71,12 @@ class _CellCalendarWidget extends StatefulWidget { class _CellCalendarWidgetState extends State<_CellCalendarWidget> { final PopoverMutex popoverMutex = PopoverMutex(); + @override + void dispose() { + popoverMutex.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -94,521 +85,50 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> { cellData: widget.cellContext.getCellData(), cellController: widget.cellContext, )..add(const DateCellCalendarEvent.initial()), - child: Padding( - padding: const EdgeInsets.only(top: 18.0, bottom: 12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - StartTextField(popoverMutex: popoverMutex), - EndTextField(popoverMutex: popoverMutex), - const DatePicker(), - const TypeOptionSeparator(spacing: 12.0), - const EndTimeButton(), - const VSpace(4.0), - const _IncludeTimeButton(), - const TypeOptionSeparator(spacing: 8.0), - DateTypeOptionButton(popoverMutex: popoverMutex), - const VSpace(4.0), - const ClearDateButton(), - ], - ), - ), - ); - } - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } -} - -class StartTextField extends StatelessWidget { - final PopoverMutex popoverMutex; - const StartTextField({super.key, required this.popoverMutex}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: state.includeTime - ? _TimeTextField( - isEndTime: false, - timeStr: state.timeStr, - popoverMutex: popoverMutex, - ) - : const SizedBox.shrink(), - ); - }, - ); - } -} - -class EndTextField extends StatelessWidget { - final PopoverMutex popoverMutex; - const EndTextField({super.key, required this.popoverMutex}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: state.includeTime && state.isRange - ? Padding( - padding: const EdgeInsets.only(top: 8.0), - child: _TimeTextField( - isEndTime: true, - timeStr: state.endTimeStr, - popoverMutex: popoverMutex, - ), - ) - : const SizedBox.shrink(), - ); - }, - ); - } -} - -class DatePicker extends StatefulWidget { - const DatePicker({super.key}); - - @override - State createState() => _DatePickerState(); -} - -class _DatePickerState extends State { - DateTime _focusedDay = DateTime.now(); - CalendarFormat _calendarFormat = CalendarFormat.month; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final textStyle = Theme.of(context).textTheme.bodyMedium!; - final boxDecoration = BoxDecoration( - color: Theme.of(context).cardColor, - shape: BoxShape.circle, - ); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TableCalendar( - firstDay: kFirstDay, - lastDay: kLastDay, - focusedDay: _focusedDay, - rowHeight: 26.0 + 7.0, - calendarFormat: _calendarFormat, - daysOfWeekHeight: 17.0 + 8.0, - rangeSelectionMode: state.isRange - ? RangeSelectionMode.enforced - : RangeSelectionMode.disabled, - rangeStartDay: state.isRange ? state.startDay : null, - rangeEndDay: state.isRange ? state.endDay : null, - headerStyle: HeaderStyle( - formatButtonVisible: false, - titleCentered: true, - titleTextStyle: textStyle, - leftChevronMargin: EdgeInsets.zero, - leftChevronPadding: EdgeInsets.zero, - leftChevronIcon: FlowySvg( - FlowySvgs.arrow_left_s, - color: Theme.of(context).iconTheme.color, - ), - rightChevronPadding: EdgeInsets.zero, - rightChevronMargin: EdgeInsets.zero, - rightChevronIcon: FlowySvg( - FlowySvgs.arrow_right_s, - color: Theme.of(context).iconTheme.color, - ), - headerMargin: EdgeInsets.zero, - headerPadding: const EdgeInsets.only(bottom: 8.0), - ), - calendarStyle: CalendarStyle( - cellMargin: const EdgeInsets.all(3.5), - defaultDecoration: boxDecoration, - selectedDecoration: boxDecoration.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - todayDecoration: boxDecoration.copyWith( - color: Colors.transparent, - border: - Border.all(color: Theme.of(context).colorScheme.primary), - ), - weekendDecoration: boxDecoration, - outsideDecoration: boxDecoration, - rangeStartDecoration: boxDecoration.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - rangeEndDecoration: boxDecoration.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - defaultTextStyle: textStyle, - weekendTextStyle: textStyle, - selectedTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - rangeStartTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - rangeEndTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - todayTextStyle: textStyle, - outsideTextStyle: textStyle.copyWith( - color: Theme.of(context).disabledColor, - ), - rangeHighlightColor: - Theme.of(context).colorScheme.secondaryContainer, - ), - calendarBuilders: CalendarBuilders( - dowBuilder: (context, day) { - final locale = context.locale.toLanguageTag(); - final label = DateFormat.E(locale).format(day).substring(0, 2); - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Center( - child: Text( - label, - style: AFThemeExtension.of(context).caption, - ), - ), - ); - }, - ), - selectedDayPredicate: (day) => - state.isRange ? false : isSameDay(state.dateTime, day), - onDaySelected: (selectedDay, focusedDay) { - context.read().add( - DateCellCalendarEvent.selectDay(selectedDay), - ); - }, - onRangeSelected: (start, end, focusedDay) { - context.read().add( - DateCellCalendarEvent.selectDateRange(start, end), - ); - }, - onFormatChanged: (calendarFormat) => setState(() { - _calendarFormat = calendarFormat; - }), - onPageChanged: (focusedDay) => setState(() { - _focusedDay = focusedDay; - }), - ), - ); - }, - ); - } -} - -class _IncludeTimeButton extends StatelessWidget { - const _IncludeTimeButton(); - - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) => state.includeTime, - builder: (context, includeTime) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: IncludeTimeButton( - onChanged: (value) => context + child: BlocBuilder( + builder: (context, state) { + return AppFlowyDatePicker( + includeTime: state.includeTime, + onIncludeTimeChanged: (value) => context .read() .add(DateCellCalendarEvent.setIncludeTime(!value)), - value: includeTime, - ), - ); - }, - ); - } -} - -@visibleForTesting -class EndTimeButton extends StatelessWidget { - const EndTimeButton({super.key}); - - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) => state.isRange, - builder: (context, isRange) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: GridSize.typeOptionContentInsets, - child: Row( - children: [ - FlowySvg( - FlowySvgs.date_s, - color: Theme.of(context).iconTheme.color, - ), - const HSpace(6), - FlowyText.medium(LocaleKeys.grid_field_isRange.tr()), - const Spacer(), - Toggle( - value: isRange, - onChanged: (value) => context - .read() - .add(DateCellCalendarEvent.setIsRange(!value)), - style: ToggleStyle.big, - padding: EdgeInsets.zero, - ), - ], - ), - ), - ), - ); - }, - ); - } -} - -const _maxLengthTwelveHour = 8; -const _maxLengthTwentyFourHour = 5; - -class _TimeTextField extends StatefulWidget { - final bool isEndTime; - final String? timeStr; - final PopoverMutex popoverMutex; - - const _TimeTextField({ - required this.timeStr, - required this.popoverMutex, - required this.isEndTime, - }); - - @override - State<_TimeTextField> createState() => _TimeTextFieldState(); -} - -class _TimeTextFieldState extends State<_TimeTextField> { - late final FocusNode _focusNode; - late final TextEditingController _textController; - - @override - void initState() { - _focusNode = FocusNode(); - _textController = TextEditingController()..text = widget.timeStr ?? ""; - - _focusNode.addListener(() { - if (_focusNode.hasFocus) { - widget.popoverMutex.close(); - } - }); - - widget.popoverMutex.listenOnPopoverChanged(() { - if (_focusNode.hasFocus) { - _focusNode.unfocus(); - } - }); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (widget.isEndTime) { - _textController.text = state.endTimeStr ?? ""; - } else { - _textController.text = state.timeStr ?? ""; - } - }, - builder: (context, state) { - String text = ""; - if (!widget.isEndTime && state.timeStr != null) { - text = state.timeStr!; - } else if (state.endTimeStr != null) { - text = state.endTimeStr!; - } - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: FlowyTextField( - text: text, - focusNode: _focusNode, - controller: _textController, - submitOnLeave: true, - hintText: state.timeHintText, - errorText: widget.isEndTime - ? state.parseEndTimeError - : state.parseTimeError, - maxLength: - state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwelveHour - ? _maxLengthTwelveHour - : _maxLengthTwentyFourHour, - showCounter: false, - onSubmitted: (timeStr) { - if (widget.isEndTime) { - context - .read() - .add(DateCellCalendarEvent.setEndTime(timeStr)); - } else { - context - .read() - .add(DateCellCalendarEvent.setTime(timeStr)); - } - }, - ), - ); - }, - ); - } - - @override - void dispose() { - _textController.dispose(); - _focusNode.removeListener(() { - if (_focusNode.hasFocus) { - widget.popoverMutex.close(); - } - }); - _focusNode.dispose(); - super.dispose(); - } -} - -@visibleForTesting -class DateTypeOptionButton extends StatelessWidget { - final PopoverMutex popoverMutex; - const DateTypeOptionButton({ - required this.popoverMutex, - super.key, - }); - - @override - Widget build(BuildContext context) { - final title = - "${LocaleKeys.grid_field_dateFormat.tr()} & ${LocaleKeys.grid_field_timeFormat.tr()}"; - return BlocSelector( - selector: (state) => state.dateTypeOptionPB, - builder: (context, dateTypeOptionPB) { - return AppFlowyPopover( - mutex: popoverMutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - margin: EdgeInsets.zero, - constraints: BoxConstraints.loose(const Size(140, 100)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText.medium(title), - rightIcon: const FlowySvg(FlowySvgs.more_s), - ), - ), - ), - popupBuilder: (BuildContext popContext) { - return _CalDateTimeSetting( - dateTypeOptionPB: dateTypeOptionPB, - onEvent: (event) { - context.read().add(event); - popoverMutex.close(); - }, - ); - }, - ); - }, - ); - } -} - -class _CalDateTimeSetting extends StatefulWidget { - final DateTypeOptionPB dateTypeOptionPB; - final Function(DateCellCalendarEvent) onEvent; - const _CalDateTimeSetting({ - required this.dateTypeOptionPB, - required this.onEvent, - }); - - @override - State<_CalDateTimeSetting> createState() => _CalDateTimeSettingState(); -} - -class _CalDateTimeSettingState extends State<_CalDateTimeSetting> { - final timeSettingPopoverMutex = PopoverMutex(); - String? overlayIdentifier; - - @override - Widget build(BuildContext context) { - final List children = [ - AppFlowyPopover( - mutex: timeSettingPopoverMutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - popupBuilder: (BuildContext context) { - return DateFormatList( - selectedFormat: widget.dateTypeOptionPB.dateFormat, - onSelected: (format) { - widget.onEvent(DateCellCalendarEvent.setDateFormat(format)); - timeSettingPopoverMutex.close(); - }, - ); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 6.0), - child: DateFormatButton(), - ), - ), - AppFlowyPopover( - mutex: timeSettingPopoverMutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - popupBuilder: (BuildContext context) { - return TimeFormatList( - selectedFormat: widget.dateTypeOptionPB.timeFormat, - onSelected: (format) { - widget.onEvent(DateCellCalendarEvent.setTimeFormat(format)); - timeSettingPopoverMutex.close(); - }, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: - TimeFormatButton(timeFormat: widget.dateTypeOptionPB.timeFormat), - ), - ), - ]; - - return SizedBox( - width: 180, - child: ListView.separated( - shrinkWrap: true, - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - itemCount: children.length, - itemBuilder: (BuildContext context, int index) => children[index], - padding: const EdgeInsets.symmetric(vertical: 6.0), - ), - ); - } -} - -@visibleForTesting -class ClearDateButton extends StatelessWidget { - const ClearDateButton({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText.medium(LocaleKeys.grid_field_clearDate.tr()), - onTap: () { - context + isRange: state.isRange, + onIsRangeChanged: (value) => context .read() - .add(const DateCellCalendarEvent.clearDate()); - PopoverContainer.of(context).close(); - }, - ), + .add(DateCellCalendarEvent.setIsRange(!value)), + dateFormat: state.dateTypeOptionPB.dateFormat, + timeFormat: state.dateTypeOptionPB.timeFormat, + selectedDay: state.dateTime, + timeStr: state.timeStr, + endTimeStr: state.endTimeStr, + timeHintText: state.timeHintText, + parseEndTimeError: state.parseEndTimeError, + parseTimeError: state.parseTimeError, + popoverMutex: popoverMutex, + onStartTimeSubmitted: (timeStr) => context + .read() + .add(DateCellCalendarEvent.setTime(timeStr)), + onEndTimeSubmitted: (timeStr) => context + .read() + .add(DateCellCalendarEvent.setEndTime(timeStr)), + onDaySelected: (selectedDay, _) => context + .read() + .add(DateCellCalendarEvent.selectDay(selectedDay)), + onRangeSelected: (start, end, _) => context + .read() + .add(DateCellCalendarEvent.selectDateRange(start, end)), + allowFormatChanges: true, + onDateFormatChanged: (format) => context + .read() + .add(DateCellCalendarEvent.setDateFormat(format)), + onTimeFormatChanged: (format) => context + .read() + .add(DateCellCalendarEvent.setTimeFormat(format)), + onClearDate: () => context + .read() + .add(const DateCellCalendarEvent.clearDate()), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/mobile_date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/mobile_date_editor.dart index f676937885..57124002c6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/mobile_date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/mobile_date_editor.dart @@ -1,8 +1,9 @@ +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_calendar.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:table_calendar/table_calendar.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart index c3aa568847..97725ff055 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; + import 'package:appflowy/plugins/document/application/doc_service.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart'; @@ -19,7 +21,6 @@ import 'package:appflowy_editor/appflowy_editor.dart' Position, paragraphNode; import 'package:dartz/dartz.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; 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 a3cc07fc40..aa808f5ead 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 @@ -1,18 +1,23 @@ +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart'; +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_popover/appflowy_popover.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:calendar_view/calendar_view.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class MentionDateBlock extends StatelessWidget { +class MentionDateBlock extends StatefulWidget { const MentionDateBlock({ super.key, required this.editorContext, @@ -37,11 +42,19 @@ class MentionDateBlock extends StatelessWidget { final bool includeTime; + @override + State createState() => _MentionDateBlockState(); +} + +class _MentionDateBlockState extends State { + late bool includeTime = widget.includeTime; + final PopoverMutex mutex = PopoverMutex(); + @override Widget build(BuildContext context) { final editorState = context.read(); - DateTime? parsedDate = DateTime.tryParse(date); + DateTime? parsedDate = DateTime.tryParse(widget.date); if (parsedDate == null) { return const SizedBox.shrink(); } @@ -62,48 +75,71 @@ class MentionDateBlock extends StatelessWidget { builder: (context, appearance) => BlocBuilder( builder: (context, state) { - final reminder = - state.reminders.firstWhereOrNull((r) => r.id == reminderId); - final noReminder = reminder == null && isReminder; + final reminder = state.reminders + .firstWhereOrNull((r) => r.id == widget.reminderId); + final noReminder = reminder == null && widget.isReminder; final formattedDate = appearance.dateFormat .formatDate(parsedDate!, includeTime, appearance.timeFormat); + final timeStr = parsedDate != null + ? _timeFromDate(parsedDate!, appearance.timeFormat) + : null; + final options = DatePickerOptions( - selectedDay: parsedDate, focusedDay: parsedDate, - firstDay: isReminder + popoverMutex: mutex, + selectedDay: parsedDate, + firstDay: widget.isReminder ? noReminder ? parsedDate : DateTime.now() : null, lastDay: noReminder ? parsedDate : null, + timeStr: timeStr, includeTime: includeTime, + enableRanges: false, + dateFormat: appearance.dateFormat, timeFormat: appearance.timeFormat, onIncludeTimeChanged: (includeTime) { + this.includeTime = includeTime; _updateBlock(parsedDate!.withoutTime, includeTime); // We can remove time from the date/reminder // block when toggled off. - if (isReminder) { + if (widget.isReminder) { _updateScheduledAt( - reminderId: reminderId!, + reminderId: widget.reminderId!, selectedDay: includeTime ? parsedDate! : parsedDate!.withoutTime, includeTime: includeTime, ); } }, - onDaySelected: (selectedDay, focusedDay, includeTime) { - parsedDate = selectedDay; + onStartTimeChanged: (time) { + final parsed = _parseTime(time, appearance.timeFormat); + parsedDate = parsedDate!.withoutTime + .add(Duration(hours: parsed.hour, minutes: parsed.minute)); + _updateBlock(parsedDate!, includeTime); + + if (widget.isReminder && + widget.date != parsedDate!.toIso8601String()) { + _updateScheduledAt( + reminderId: widget.reminderId!, + selectedDay: parsedDate!, + ); + } + }, + onDaySelected: (selectedDay, focusedDay) { + parsedDate = selectedDay; _updateBlock(selectedDay, includeTime); - if (isReminder && date != selectedDay.toIso8601String()) { + if (widget.isReminder && + widget.date != selectedDay.toIso8601String()) { _updateScheduledAt( - reminderId: reminderId!, + reminderId: widget.reminderId!, selectedDay: selectedDay, - includeTime: includeTime, ); } }, @@ -124,9 +160,11 @@ class MentionDateBlock extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ FlowySvg( - isReminder ? FlowySvgs.clock_alarm_s : FlowySvgs.date_s, + widget.isReminder + ? FlowySvgs.clock_alarm_s + : FlowySvgs.date_s, size: const Size.square(18.0), - color: isReminder && reminder?.isAck == true + color: widget.isReminder && reminder?.isAck == true ? Theme.of(context).colorScheme.error : null, ), @@ -134,7 +172,7 @@ class MentionDateBlock extends StatelessWidget { FlowyText( formattedDate, fontSize: fontSize, - color: isReminder && reminder?.isAck == true + color: widget.isReminder && reminder?.isAck == true ? Theme.of(context).colorScheme.error : null, ), @@ -149,18 +187,41 @@ class MentionDateBlock extends StatelessWidget { ); } + DateTime _parseTime(String timeStr, UserTimeFormatPB timeFormat) { + final twelveHourFormat = DateFormat('HH:mm a'); + final twentyFourHourFormat = DateFormat('HH:mm'); + + if (timeFormat == TimeFormatPB.TwelveHour) { + return twelveHourFormat.parse(timeStr); + } + + return twentyFourHourFormat.parse(timeStr); + } + + String _timeFromDate(DateTime date, UserTimeFormatPB timeFormat) { + final twelveHourFormat = DateFormat('HH:mm a'); + final twentyFourHourFormat = DateFormat('HH:mm'); + + if (timeFormat == TimeFormatPB.TwelveHour) { + return twelveHourFormat.format(date); + } + + return twentyFourHourFormat.format(date); + } + void _updateBlock( DateTime date, [ bool includeTime = false, ]) { - final editorState = editorContext.read(); + final editorState = widget.editorContext.read(); final transaction = editorState.transaction - ..formatText(node, index, 1, { + ..formatText(widget.node, widget.index, 1, { MentionBlockKeys.mention: { - MentionBlockKeys.type: - isReminder ? MentionType.reminder.name : MentionType.date.name, + MentionBlockKeys.type: widget.isReminder + ? MentionType.reminder.name + : MentionType.date.name, MentionBlockKeys.date: date.toIso8601String(), - MentionBlockKeys.uid: reminderId, + MentionBlockKeys.uid: widget.reminderId, MentionBlockKeys.includeTime: includeTime, }, }); @@ -180,7 +241,7 @@ class MentionDateBlock extends StatelessWidget { required DateTime selectedDay, bool? includeTime, }) { - editorContext.read().add( + widget.editorContext.read().add( ReminderEvent.update( ReminderUpdate( id: reminderId, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_calendar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_calendar.dart deleted file mode 100644 index 16d2de99ea..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_calendar.dart +++ /dev/null @@ -1,277 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:table_calendar/table_calendar.dart'; - -final kFirstDay = DateTime.utc(1970, 1, 1); -final kLastDay = DateTime.utc(2100, 1, 1); - -typedef DaySelectedCallback = void Function( - DateTime selectedDay, - DateTime focusedDay, - bool includeTime, -); -typedef IncludeTimeChangedCallback = void Function(bool includeTime); -typedef FormatChangedCallback = void Function(CalendarFormat format); -typedef PageChangedCallback = void Function(DateTime focusedDay); -typedef TimeChangedCallback = void Function(String? time); - -class AppFlowyCalendar extends StatefulWidget { - const AppFlowyCalendar({ - super.key, - this.popoverMutex, - this.firstDay, - this.lastDay, - this.selectedDate, - required this.focusedDay, - this.format = CalendarFormat.month, - this.onDaySelected, - this.onFormatChanged, - this.onPageChanged, - this.onIncludeTimeChanged, - this.onTimeChanged, - this.includeTime = false, - this.timeFormat = UserTimeFormatPB.TwentyFourHour, - }); - - final PopoverMutex? popoverMutex; - - /// Disallows choosing dates before this date - final DateTime? firstDay; - - /// Disallows choosing dates after this date - final DateTime? lastDay; - - final DateTime? selectedDate; - final DateTime focusedDay; - final CalendarFormat format; - - final DaySelectedCallback? onDaySelected; - final IncludeTimeChangedCallback? onIncludeTimeChanged; - final FormatChangedCallback? onFormatChanged; - final PageChangedCallback? onPageChanged; - final TimeChangedCallback? onTimeChanged; - - final bool includeTime; - - // Timeformat for time selector - final UserTimeFormatPB timeFormat; - - @override - State createState() => _AppFlowyCalendarState(); -} - -class _AppFlowyCalendarState extends State - with AutomaticKeepAliveClientMixin { - String? _time; - - late DateTime? _selectedDay = widget.selectedDate; - late DateTime _focusedDay = widget.focusedDay; - late bool _includeTime = widget.includeTime; - - @override - void initState() { - super.initState(); - if (widget.includeTime) { - final hour = widget.focusedDay.hour; - final minute = widget.focusedDay.minute; - _time = '$hour:$minute'; - } - } - - @override - Widget build(BuildContext context) { - super.build(context); - - final textStyle = Theme.of(context).textTheme.bodyMedium!; - final boxDecoration = BoxDecoration( - color: Theme.of(context).cardColor, - shape: BoxShape.circle, - ); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const VSpace(18), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: TableCalendar( - currentDay: DateTime.now(), - firstDay: widget.firstDay ?? kFirstDay, - lastDay: widget.lastDay ?? kLastDay, - focusedDay: _focusedDay, - rowHeight: GridSize.popoverItemHeight, - calendarFormat: widget.format, - daysOfWeekHeight: GridSize.popoverItemHeight, - headerStyle: HeaderStyle( - formatButtonVisible: false, - titleCentered: true, - titleTextStyle: textStyle, - leftChevronMargin: EdgeInsets.zero, - leftChevronPadding: EdgeInsets.zero, - leftChevronIcon: FlowySvg( - FlowySvgs.arrow_left_s, - color: Theme.of(context).iconTheme.color, - ), - rightChevronPadding: EdgeInsets.zero, - rightChevronMargin: EdgeInsets.zero, - rightChevronIcon: FlowySvg( - FlowySvgs.arrow_right_s, - color: Theme.of(context).iconTheme.color, - ), - headerMargin: EdgeInsets.zero, - headerPadding: const EdgeInsets.only(bottom: 8.0), - ), - calendarStyle: CalendarStyle( - cellMargin: const EdgeInsets.all(3.5), - defaultDecoration: boxDecoration, - selectedDecoration: boxDecoration.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - todayDecoration: boxDecoration.copyWith( - color: Colors.transparent, - border: Border.all( - color: Theme.of(context).colorScheme.primary, - ), - ), - weekendDecoration: boxDecoration, - outsideDecoration: boxDecoration, - rangeStartDecoration: boxDecoration.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - rangeEndDecoration: boxDecoration.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - defaultTextStyle: textStyle, - weekendTextStyle: textStyle, - selectedTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - rangeStartTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - rangeEndTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - todayTextStyle: textStyle, - outsideTextStyle: textStyle.copyWith( - color: Theme.of(context).disabledColor, - ), - rangeHighlightColor: - Theme.of(context).colorScheme.secondaryContainer, - ), - calendarBuilders: CalendarBuilders( - dowBuilder: (context, day) { - final locale = context.locale.toLanguageTag(); - final label = DateFormat.E(locale).format(day).substring(0, 2); - - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Center( - child: Text( - label, - style: AFThemeExtension.of(context).caption, - ), - ), - ); - }, - ), - selectedDayPredicate: (day) => isSameDay(_selectedDay, day), - onDaySelected: (selectedDay, focusedDay) { - if (!_includeTime) { - widget.onDaySelected?.call( - selectedDay, - focusedDay, - _includeTime, - ); - } - - setState(() { - _selectedDay = selectedDay; - _focusedDay = focusedDay; - }); - - _updateSelectedDay(selectedDay, focusedDay, _includeTime); - }, - onFormatChanged: widget.onFormatChanged, - onPageChanged: widget.onPageChanged, - ), - ), - const TypeOptionSeparator(spacing: 12.0), - IncludeTimeButton( - initialTime: widget.selectedDate != null - ? _initialTime(widget.selectedDate!) - : null, - includeTime: widget.includeTime, - timeFormat: widget.timeFormat, - popoverMutex: widget.popoverMutex, - onChanged: (includeTime) { - setState(() => _includeTime = includeTime); - - widget.onIncludeTimeChanged?.call(includeTime); - }, - onSubmitted: (time) { - _time = time; - - if (widget.selectedDate != null && widget.onTimeChanged == null) { - _updateSelectedDay( - widget.selectedDate!, - widget.selectedDate!, - _includeTime, - ); - } - - widget.onTimeChanged?.call(time); - }, - ), - const VSpace(6.0), - ], - ); - } - - DateTime _dateWithTime(DateTime date, DateTime time) { - return DateTime.parse( - '${date.year}${_padZeroLeft(date.month)}${_padZeroLeft(date.day)} ${_padZeroLeft(time.hour)}:${_padZeroLeft(time.minute)}', - ); - } - - String _initialTime(DateTime selectedDay) => switch (widget.timeFormat) { - UserTimeFormatPB.TwelveHour => DateFormat.jm().format(selectedDay), - UserTimeFormatPB.TwentyFourHour => DateFormat.Hm().format(selectedDay), - _ => '00:00', - }; - - String _padZeroLeft(int a) => a.toString().padLeft(2, '0'); - - void _updateSelectedDay( - DateTime selectedDay, - DateTime focusedDay, - bool includeTime, - ) { - late DateTime timeOfDay; - switch (widget.timeFormat) { - case UserTimeFormatPB.TwelveHour: - timeOfDay = DateFormat.jm().parse(_time ?? '12:00 AM'); - break; - case UserTimeFormatPB.TwentyFourHour: - timeOfDay = DateFormat.Hm().parse(_time ?? '00:00'); - break; - } - - widget.onDaySelected?.call( - _dateWithTime(selectedDay, timeOfDay), - focusedDay, - _includeTime, - ); - } - - @override - bool get wantKeepAlive => true; -} 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 new file mode 100644 index 0000000000..800b88b454 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.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_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +typedef DaySelectedCallback = Function(DateTime, DateTime); +typedef RangeSelectedCallback = Function(DateTime?, DateTime?, DateTime); +typedef IncludeTimeChangedCallback = Function(bool); +typedef TimeChangedCallback = Function(String); + +class AppFlowyDatePicker extends StatefulWidget { + const AppFlowyDatePicker({ + super.key, + required this.includeTime, + required this.onIncludeTimeChanged, + this.rebuildOnDaySelected = true, + this.enableRanges = true, + this.isRange = false, + this.onIsRangeChanged, + required this.dateFormat, + required this.timeFormat, + this.selectedDay, + this.focusedDay, + this.firstDay, + this.lastDay, + this.timeStr, + this.endTimeStr, + this.timeHintText, + this.parseEndTimeError, + this.parseTimeError, + this.popoverMutex, + this.onStartTimeSubmitted, + this.onEndTimeSubmitted, + this.onDaySelected, + this.onRangeSelected, + this.allowFormatChanges = false, + this.onDateFormatChanged, + this.onTimeFormatChanged, + this.onClearDate, + }); + + final bool includeTime; + final Function(bool) onIncludeTimeChanged; + + final bool enableRanges; + final bool isRange; + final Function(bool)? onIsRangeChanged; + + final bool rebuildOnDaySelected; + + final DateFormatPB dateFormat; + final TimeFormatPB timeFormat; + + final DateTime? selectedDay; + final DateTime? focusedDay; + final DateTime? firstDay; + final DateTime? lastDay; + final String? timeStr; + final String? endTimeStr; + final String? timeHintText; + final String? parseEndTimeError; + final String? parseTimeError; + final PopoverMutex? popoverMutex; + + final TimeChangedCallback? onStartTimeSubmitted; + final TimeChangedCallback? onEndTimeSubmitted; + final DaySelectedCallback? onDaySelected; + final RangeSelectedCallback? onRangeSelected; + + /// If this value is true, then [onTimeFormatChanged] and [onDateFormatChanged] + /// cannot be null + /// + final bool allowFormatChanges; + + /// If [allowFormatChanges] is true, this must be provided + /// + final Function(DateFormatPB)? onDateFormatChanged; + + /// If [allowFormatChanges] is true, this must be provided + /// + final Function(TimeFormatPB)? onTimeFormatChanged; + + /// If provided, the ClearDate button will be shown + /// Otherwise it will be hidden + /// + final VoidCallback? onClearDate; + + @override + State createState() => _AppFlowyDatePickerState(); +} + +class _AppFlowyDatePickerState extends State { + late DateTime? _selectedDay = widget.selectedDay; + + @override + void didChangeDependencies() { + _selectedDay = widget.selectedDay; + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + 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: _selectedDay, + firstDay: widget.firstDay, + lastDay: widget.lastDay, + ), + 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, + ), + ), + if (widget.onClearDate != null || + (widget.allowFormatChanges && + widget.onDateFormatChanged != null && + widget.onTimeFormatChanged != null)) + // Only show if either of the options are below it + const TypeOptionSeparator(spacing: 8.0), + if (widget.allowFormatChanges && + widget.onDateFormatChanged != null && + widget.onTimeFormatChanged != null) + DateTypeOptionButton( + popoverMutex: widget.popoverMutex, + dateFormat: widget.dateFormat, + timeFormat: widget.timeFormat, + onDateFormatChanged: widget.onDateFormatChanged!, + onTimeFormatChanged: widget.onTimeFormatChanged!, + ), + if (widget.onClearDate != null) ...[ + const VSpace(4.0), + ClearDateButton(onClearDate: widget.onClearDate!), + ], + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart new file mode 100644 index 0000000000..36657a4321 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart @@ -0,0 +1,13 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; + +extension ToDateFormat on UserDateFormatPB { + DateFormatPB get simplified => switch (this) { + UserDateFormatPB.DayMonthYear => DateFormatPB.DayMonthYear, + UserDateFormatPB.Friendly => DateFormatPB.Friendly, + UserDateFormatPB.ISO => DateFormatPB.ISO, + UserDateFormatPB.Locally => DateFormatPB.Local, + UserDateFormatPB.US => DateFormatPB.US, + _ => DateFormatPB.Friendly, + }; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/layout.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/layout.dart new file mode 100644 index 0000000000..7dde3368f2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/layout.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class DatePickerSize { + static double scale = 1; + + static double get itemHeight => 26 * scale; + static double get seperatorHeight => 4 * scale; + + static EdgeInsets get itemOptionInsets => const EdgeInsets.all(4); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart new file mode 100644 index 0000000000..2040785371 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart @@ -0,0 +1,10 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; + +extension ToTimeFormat on UserTimeFormatPB { + TimeFormatPB get simplified => switch (this) { + UserTimeFormatPB.TwelveHour => TimeFormatPB.TwelveHour, + UserTimeFormatPB.TwentyFourHour => TimeFormatPB.TwentyFourHour, + _ => TimeFormatPB.TwentyFourHour, + }; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart new file mode 100644 index 0000000000..c3157c0933 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart @@ -0,0 +1,34 @@ +import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; + +class ClearDateButton extends StatelessWidget { + const ClearDateButton({ + super.key, + required this.onClearDate, + }); + + final VoidCallback onClearDate; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: DatePickerSize.itemHeight, + child: FlowyButton( + text: FlowyText.medium(LocaleKeys.datePicker_clearDate.tr()), + onTap: () { + onClearDate(); + PopoverContainer.of(context).close(); + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart new file mode 100644 index 0000000000..62b8eb85f9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:table_calendar/table_calendar.dart'; + +final kFirstDay = DateTime.utc(1970, 1, 1); +final kLastDay = DateTime.utc(2100, 1, 1); + +class DatePicker extends StatefulWidget { + const DatePicker({ + super.key, + required this.isRange, + this.calendarFormat = CalendarFormat.month, + this.startDay, + this.endDay, + this.selectedDay, + this.firstDay, + this.lastDay, + this.onDaySelected, + this.onRangeSelected, + }); + + final bool isRange; + final CalendarFormat calendarFormat; + + final DateTime? startDay; + final DateTime? endDay; + final DateTime? selectedDay; + + /// If not provided, defaults to 1st January 1970 + /// + final DateTime? firstDay; + + /// If not provided, defaults to 1st January 2100 + /// + final DateTime? lastDay; + + final Function( + DateTime selectedDay, + DateTime focusedDay, + )? onDaySelected; + + final Function( + DateTime? start, + DateTime? end, + DateTime focusedDay, + )? onRangeSelected; + + @override + State createState() => _DatePickerState(); +} + +class _DatePickerState extends State { + DateTime _focusedDay = DateTime.now(); + late CalendarFormat _calendarFormat = widget.calendarFormat; + + @override + Widget build(BuildContext context) { + final textStyle = Theme.of(context).textTheme.bodyMedium!; + final boxDecoration = BoxDecoration( + color: Theme.of(context).cardColor, + shape: BoxShape.circle, + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TableCalendar( + firstDay: widget.firstDay ?? kFirstDay, + lastDay: widget.lastDay ?? kLastDay, + focusedDay: _focusedDay, + rowHeight: 26.0 + 7.0, + calendarFormat: _calendarFormat, + availableCalendarFormats: const {CalendarFormat.month: 'Month'}, + daysOfWeekHeight: 17.0 + 8.0, + rangeSelectionMode: widget.isRange + ? RangeSelectionMode.enforced + : RangeSelectionMode.disabled, + rangeStartDay: widget.isRange ? widget.startDay : null, + rangeEndDay: widget.isRange ? widget.endDay : null, + headerStyle: HeaderStyle( + formatButtonVisible: false, + titleCentered: true, + titleTextStyle: textStyle, + leftChevronMargin: EdgeInsets.zero, + leftChevronPadding: EdgeInsets.zero, + leftChevronIcon: FlowySvg( + FlowySvgs.arrow_left_s, + color: Theme.of(context).iconTheme.color, + ), + rightChevronPadding: EdgeInsets.zero, + rightChevronMargin: EdgeInsets.zero, + rightChevronIcon: FlowySvg( + FlowySvgs.arrow_right_s, + color: Theme.of(context).iconTheme.color, + ), + headerMargin: EdgeInsets.zero, + headerPadding: const EdgeInsets.only(bottom: 8.0), + ), + calendarStyle: CalendarStyle( + cellMargin: const EdgeInsets.all(3.5), + defaultDecoration: boxDecoration, + selectedDecoration: boxDecoration.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + todayDecoration: boxDecoration.copyWith( + color: Colors.transparent, + border: Border.all(color: Theme.of(context).colorScheme.primary), + ), + weekendDecoration: boxDecoration, + outsideDecoration: boxDecoration, + rangeStartDecoration: boxDecoration.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + rangeEndDecoration: boxDecoration.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + defaultTextStyle: textStyle, + weekendTextStyle: textStyle, + selectedTextStyle: textStyle.copyWith( + color: Theme.of(context).colorScheme.surface, + ), + rangeStartTextStyle: textStyle.copyWith( + color: Theme.of(context).colorScheme.surface, + ), + rangeEndTextStyle: textStyle.copyWith( + color: Theme.of(context).colorScheme.surface, + ), + todayTextStyle: textStyle, + outsideTextStyle: textStyle.copyWith( + color: Theme.of(context).disabledColor, + ), + rangeHighlightColor: Theme.of(context).colorScheme.secondaryContainer, + ), + calendarBuilders: CalendarBuilders( + dowBuilder: (context, day) { + final locale = context.locale.toLanguageTag(); + final label = DateFormat.E(locale).format(day).substring(0, 2); + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Center( + child: Text( + label, + style: AFThemeExtension.of(context).caption, + ), + ), + ); + }, + ), + selectedDayPredicate: (day) => + widget.isRange ? false : isSameDay(widget.selectedDay, day), + onFormatChanged: (calendarFormat) => + setState(() => _calendarFormat = calendarFormat), + onPageChanged: (focusedDay) => setState(() => _focusedDay = focusedDay), + onDaySelected: widget.onDaySelected, + onRangeSelected: widget.onRangeSelected, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart index 6f4abf3b95..bd03e6af31 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart @@ -1,40 +1,53 @@ -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_calendar.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -/// Provides arguemnts for [AppFlowyCalender] when showing +import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart'; +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_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; + +/// Provides arguemnts for [AppFlowyDatePicker] when showing /// a [DatePickerMenu] /// class DatePickerOptions { DatePickerOptions({ DateTime? focusedDay, + this.popoverMutex, this.selectedDay, this.firstDay, this.lastDay, + this.timeStr, this.includeTime = false, + this.isRange = false, + this.enableRanges = true, + this.dateFormat = UserDateFormatPB.Friendly, this.timeFormat = UserTimeFormatPB.TwentyFourHour, this.onDaySelected, this.onIncludeTimeChanged, - this.onFormatChanged, - this.onPageChanged, - this.onTimeChanged, + this.onStartTimeChanged, + this.onEndTimeChanged, }) : focusedDay = focusedDay ?? DateTime.now(); final DateTime focusedDay; + final PopoverMutex? popoverMutex; final DateTime? selectedDay; final DateTime? firstDay; final DateTime? lastDay; + final String? timeStr; final bool includeTime; + final bool isRange; + final bool enableRanges; + final UserDateFormatPB dateFormat; final UserTimeFormatPB timeFormat; final DaySelectedCallback? onDaySelected; final IncludeTimeChangedCallback? onIncludeTimeChanged; - final FormatChangedCallback? onFormatChanged; - final PageChangedCallback? onPageChanged; - final TimeChangedCallback? onTimeChanged; + final TimeChangedCallback? onStartTimeChanged; + final TimeChangedCallback? onEndTimeChanged; } abstract class DatePickerService { @@ -43,8 +56,8 @@ abstract class DatePickerService { } const double _datePickerWidth = 260; -const double _datePickerHeight = 325; -const double _includeTimeHeight = 60; +const double _datePickerHeight = 355; +const double _includeTimeHeight = 40; const double _ySpacing = 15; class DatePickerMenu extends DatePickerService { @@ -175,22 +188,27 @@ class _AnimatedDatePickerState extends State<_AnimatedDatePicker> { constraints: BoxConstraints.loose( const Size(_datePickerWidth, 465), ), - child: AppFlowyCalendar( + child: AppFlowyDatePicker( + popoverMutex: widget.options?.popoverMutex, + includeTime: _includeTime, + enableRanges: widget.options?.enableRanges ?? false, + isRange: widget.options?.isRange ?? false, + onIsRangeChanged: (_) {}, + timeStr: widget.options?.timeStr, + dateFormat: + widget.options?.dateFormat.simplified ?? DateFormatPB.Friendly, + timeFormat: widget.options?.timeFormat.simplified ?? + TimeFormatPB.TwentyFourHour, + selectedDay: widget.options?.selectedDay, + onIncludeTimeChanged: (includeTime) { + widget.options?.onIncludeTimeChanged?.call(!includeTime); + setState(() => _includeTime = !includeTime); + }, + onStartTimeSubmitted: widget.options?.onStartTimeChanged, + onDaySelected: widget.options?.onDaySelected, focusedDay: widget.options?.focusedDay ?? DateTime.now(), - selectedDate: widget.options?.selectedDay, firstDay: widget.options?.firstDay, lastDay: widget.options?.lastDay, - includeTime: widget.options?.includeTime ?? false, - timeFormat: - widget.options?.timeFormat ?? UserTimeFormatPB.TwentyFourHour, - onDaySelected: widget.options?.onDaySelected, - onFormatChanged: widget.options?.onFormatChanged, - onPageChanged: widget.options?.onPageChanged, - onIncludeTimeChanged: (includeTime) { - widget.options?.onIncludeTimeChanged?.call(includeTime); - setState(() => _includeTime = includeTime); - }, - onTimeChanged: widget.options?.onTimeChanged, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart new file mode 100644 index 0000000000..39556573e2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart @@ -0,0 +1,83 @@ +import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class DateTimeSetting extends StatefulWidget { + const DateTimeSetting({ + super.key, + required this.dateFormat, + required this.timeFormat, + required this.onDateFormatChanged, + required this.onTimeFormatChanged, + }); + + final DateFormatPB dateFormat; + final TimeFormatPB timeFormat; + final Function(DateFormatPB) onDateFormatChanged; + final Function(TimeFormatPB) onTimeFormatChanged; + + @override + State createState() => _DateTimeSettingState(); +} + +class _DateTimeSettingState extends State { + final timeSettingPopoverMutex = PopoverMutex(); + String? overlayIdentifier; + + @override + Widget build(BuildContext context) { + final List children = [ + AppFlowyPopover( + mutex: timeSettingPopoverMutex, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + popupBuilder: (BuildContext context) => DateFormatList( + selectedFormat: widget.dateFormat, + onSelected: _onDateFormatChanged, + ), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 6.0), + child: DateFormatButton(), + ), + ), + AppFlowyPopover( + mutex: timeSettingPopoverMutex, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + popupBuilder: (BuildContext context) => TimeFormatList( + selectedFormat: widget.timeFormat, + onSelected: _onTimeFormatChanged, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: TimeFormatButton(timeFormat: widget.timeFormat), + ), + ), + ]; + + return SizedBox( + width: 180, + child: ListView.separated( + shrinkWrap: true, + separatorBuilder: (_, __) => VSpace(DatePickerSize.seperatorHeight), + itemCount: children.length, + itemBuilder: (_, int index) => children[index], + padding: const EdgeInsets.symmetric(vertical: 6.0), + ), + ); + } + + void _onTimeFormatChanged(TimeFormatPB format) { + widget.onTimeFormatChanged(format); + timeSettingPopoverMutex.close(); + } + + void _onDateFormatChanged(DateFormatPB format) { + widget.onDateFormatChanged(format); + timeSettingPopoverMutex.close(); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart new file mode 100644 index 0000000000..368bc787fa --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class DateTypeOptionButton extends StatelessWidget { + const DateTypeOptionButton({ + super.key, + required this.dateFormat, + required this.timeFormat, + required this.onDateFormatChanged, + required this.onTimeFormatChanged, + required this.popoverMutex, + }); + + final DateFormatPB dateFormat; + final TimeFormatPB timeFormat; + final Function(DateFormatPB) onDateFormatChanged; + final Function(TimeFormatPB) onTimeFormatChanged; + final PopoverMutex? popoverMutex; + + @override + Widget build(BuildContext context) { + final title = + "${LocaleKeys.datePicker_dateFormat.tr()} & ${LocaleKeys.datePicker_timeFormat.tr()}"; + return AppFlowyPopover( + mutex: popoverMutex, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + margin: EdgeInsets.zero, + constraints: BoxConstraints.loose(const Size(140, 100)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(title), + rightIcon: const FlowySvg(FlowySvgs.more_s), + ), + ), + ), + popupBuilder: (_) => DateTimeSetting( + dateFormat: dateFormat, + timeFormat: timeFormat, + onDateFormatChanged: (format) { + onDateFormatChanged(format); + popoverMutex?.close(); + }, + onTimeFormatChanged: (format) { + onTimeFormatChanged(format); + popoverMutex?.close(); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart new file mode 100644 index 0000000000..8f48126488 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; + +class EndTextField extends StatelessWidget { + const EndTextField({ + super.key, + required this.includeTime, + required this.isRange, + required this.timeFormat, + this.endTimeStr, + this.popoverMutex, + this.onSubmitted, + }); + + final bool includeTime; + final bool isRange; + final TimeFormatPB timeFormat; + final String? endTimeStr; + final PopoverMutex? popoverMutex; + final Function(String timeStr)? onSubmitted; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: includeTime && isRange + ? Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TimeTextField( + isEndTime: true, + timeFormat: timeFormat, + timeStr: endTimeStr, + popoverMutex: popoverMutex, + onSubmitted: onSubmitted, + ), + ) + : const SizedBox.shrink(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart new file mode 100644 index 0000000000..2887f8cbe8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart @@ -0,0 +1,50 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; + +class EndTimeButton extends StatelessWidget { + const EndTimeButton({ + super.key, + required this.isRange, + required this.onChanged, + }); + + final bool isRange; + final Function(bool value) onChanged; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: GridSize.typeOptionContentInsets, + child: Row( + children: [ + FlowySvg( + FlowySvgs.date_s, + color: Theme.of(context).iconTheme.color, + ), + const HSpace(6), + FlowyText.medium(LocaleKeys.datePicker_isRange.tr()), + const Spacer(), + Toggle( + value: isRange, + onChanged: onChanged, + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart index adc16649df..72d049cba8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart @@ -1,235 +1,48 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart'; -import 'package:appflowy/workspace/application/settings/date_time/time_patterns.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; import 'package:flutter/material.dart'; -class IncludeTimeButton extends StatefulWidget { +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +class IncludeTimeButton extends StatelessWidget { const IncludeTimeButton({ super.key, - this.initialTime, - required this.popoverMutex, - this.includeTime = false, - this.onChanged, - this.onSubmitted, - this.timeFormat = UserTimeFormatPB.TwentyFourHour, + required this.value, + required this.onChanged, }); - final String? initialTime; - final PopoverMutex? popoverMutex; - final bool includeTime; - final Function(bool includeTime)? onChanged; - final Function(String? time)? onSubmitted; - final UserTimeFormatPB timeFormat; - - @override - State createState() => _IncludeTimeButtonState(); -} - -class _IncludeTimeButtonState extends State { - late bool _includeTime = widget.includeTime; - bool _showTimeTooltip = false; - String? _timeString; - - @override - void initState() { - super.initState(); - _timeString = widget.initialTime; - } + final bool value; + final Function(bool value) onChanged; @override Widget build(BuildContext context) { - return Column( - children: [ - if (_includeTime) ...[ - _TimeTextField( - timeStr: _timeString, - popoverMutex: widget.popoverMutex, - timeFormat: widget.timeFormat, - onSubmitted: (value) { - setState(() => _timeString = value); - widget.onSubmitted?.call(_timeString); - }, - ), - const TypeOptionSeparator(spacing: 12.0), - ], - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: GridSize.typeOptionContentInsets - - const EdgeInsets.only(top: 4), - child: Row( - children: [ - FlowySvg( - FlowySvgs.clock_alarm_s, - color: Theme.of(context).iconTheme.color, - ), - const HSpace(6), - FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()), - const HSpace(6), - FlowyTooltip( - message: LocaleKeys.datePicker_dateTimeFormatTooltip.tr(), - child: FlowyHover( - resetHoverOnRebuild: false, - style: HoverStyle( - foregroundColorOnHover: - Theme.of(context).colorScheme.primary, - borderRadius: Corners.s10Border, - ), - onHover: (isHovering) => setState( - () => _showTimeTooltip = isHovering, - ), - child: FlowyTextButton( - '?', - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - fontColor: _showTimeTooltip - ? Theme.of(context).colorScheme.onSurface - : null, - fillColor: _showTimeTooltip - ? Theme.of(context).colorScheme.primary - : null, - radius: Corners.s12Border, - ), - ), - ), - const Spacer(), - Toggle( - value: _includeTime, - onChanged: (value) { - widget.onChanged?.call(!value); - setState(() => _includeTime = !value); - }, - style: ToggleStyle.big, - padding: EdgeInsets.zero, - ), - ], - ), + return SizedBox( + height: DatePickerSize.itemHeight, + child: Padding( + padding: DatePickerSize.itemOptionInsets, + child: Row( + children: [ + FlowySvg( + FlowySvgs.clock_alarm_s, + color: Theme.of(context).iconTheme.color, ), - ), + const HSpace(6), + FlowyText.medium(LocaleKeys.datePicker_includeTime.tr()), + const Spacer(), + Toggle( + value: value, + onChanged: onChanged, + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], ), - ], + ), ); } } - -const _maxLengthTwelveHour = 8; -const _maxLengthTwentyFourHour = 5; - -class _TimeTextField extends StatefulWidget { - const _TimeTextField({ - required this.timeStr, - required this.popoverMutex, - this.onSubmitted, - this.timeFormat = UserTimeFormatPB.TwentyFourHour, - }); - - final String? timeStr; - final PopoverMutex? popoverMutex; - final Function(String? value)? onSubmitted; - final UserTimeFormatPB timeFormat; - - @override - State<_TimeTextField> createState() => _TimeTextFieldState(); -} - -class _TimeTextFieldState extends State<_TimeTextField> { - late final FocusNode _focusNode; - late final TextEditingController _textController; - - late String? _timeString; - - String? errorText; - - @override - void initState() { - super.initState(); - - _timeString = widget.timeStr; - _focusNode = FocusNode(); - _textController = TextEditingController()..text = _timeString ?? ""; - - _focusNode.addListener(() { - if (_focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - }); - - widget.popoverMutex?.listenOnPopoverChanged(() { - if (_focusNode.hasFocus) { - _focusNode.unfocus(); - } - }); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: FlowyTextField( - text: _timeString ?? "", - focusNode: _focusNode, - controller: _textController, - maxLength: widget.timeFormat == UserTimeFormatPB.TwelveHour - ? _maxLengthTwelveHour - : _maxLengthTwentyFourHour, - showCounter: false, - submitOnLeave: true, - hintText: hintText, - errorText: errorText, - onSubmitted: (value) { - setState(() { - errorText = _validate(value); - }); - - if (errorText == null) { - widget.onSubmitted?.call(value); - } - }, - ), - ), - ], - ); - } - - String? _validate(String value) { - final msg = LocaleKeys.grid_field_invalidTimeFormat.tr(); - - switch (widget.timeFormat) { - case UserTimeFormatPB.TwentyFourHour: - if (!isTwentyFourHourTime(value)) { - return "$msg. e.g. 13:00"; - } - case UserTimeFormatPB.TwelveHour: - if (!isTwelveHourTime(value)) { - return "$msg. e.g. 01:00 PM"; - } - } - - return null; - } - - String get hintText => switch (widget.timeFormat) { - UserTimeFormatPB.TwentyFourHour => - LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr(), - UserTimeFormatPB.TwelveHour => - LocaleKeys.document_date_timeHintTextInTwelveHour.tr(), - _ => "", - }; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart new file mode 100644 index 0000000000..77afa8f7e1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; + +class StartTextField extends StatelessWidget { + const StartTextField({ + super.key, + required this.includeTime, + required this.timeFormat, + this.timeHintText, + this.parseEndTimeError, + this.parseTimeError, + this.timeStr, + this.endTimeStr, + this.popoverMutex, + this.onSubmitted, + }); + + final bool includeTime; + final TimeFormatPB timeFormat; + final String? timeHintText; + final String? parseEndTimeError; + final String? parseTimeError; + final String? timeStr; + final String? endTimeStr; + final PopoverMutex? popoverMutex; + final Function(String timeStr)? onSubmitted; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: includeTime + ? TimeTextField( + isEndTime: false, + timeFormat: timeFormat, + timeHintText: timeHintText, + parseEndTimeError: parseEndTimeError, + parseTimeError: parseTimeError, + timeStr: timeStr, + endTimeStr: endTimeStr, + popoverMutex: popoverMutex, + onSubmitted: onSubmitted, + ) + : const SizedBox.shrink(), + ); + } +} 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 new file mode 100644 index 0000000000..1ce1164af4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.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'; + +const _maxLengthTwelveHour = 8; +const _maxLengthTwentyFourHour = 5; + +class TimeTextField extends StatefulWidget { + const TimeTextField({ + super.key, + required this.isEndTime, + required this.timeFormat, + this.timeHintText, + this.parseEndTimeError, + this.parseTimeError, + this.timeStr, + this.endTimeStr, + this.popoverMutex, + this.onSubmitted, + }); + + final bool isEndTime; + final TimeFormatPB timeFormat; + final String? timeHintText; + final String? parseEndTimeError; + final String? parseTimeError; + final String? timeStr; + final String? endTimeStr; + final PopoverMutex? popoverMutex; + final Function(String timeStr)? onSubmitted; + + @override + State createState() => _TimeTextFieldState(); +} + +class _TimeTextFieldState extends State { + final FocusNode _focusNode = FocusNode(); + late final TextEditingController _textController = TextEditingController() + ..text = widget.timeStr ?? ""; + String text = ""; + + @override + void initState() { + super.initState(); + + if (widget.isEndTime) { + _textController.text = widget.endTimeStr ?? ""; + } else { + _textController.text = widget.timeStr ?? ""; + } + + if (!widget.isEndTime && widget.timeStr != null) { + text = widget.timeStr!; + } else if (widget.endTimeStr != null) { + text = widget.endTimeStr!; + } + + _focusNode.addListener(_focusNodeListener); + widget.popoverMutex?.listenOnPopoverChanged(_popoverListener); + } + + @override + void dispose() { + widget.popoverMutex?.removePopoverListener(_popoverListener); + _textController.dispose(); + _focusNode.removeListener(_focusNodeListener); + _focusNode.dispose(); + super.dispose(); + } + + void _focusNodeListener() { + if (_focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + } + + void _popoverListener() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: FlowyTextField( + text: text, + focusNode: _focusNode, + controller: _textController, + submitOnLeave: true, + hintText: widget.timeHintText, + errorText: + widget.isEndTime ? widget.parseEndTimeError : widget.parseTimeError, + maxLength: widget.timeFormat == TimeFormatPB.TwelveHour + ? _maxLengthTwelveHour + : _maxLengthTwentyFourHour, + showCounter: false, + onSubmitted: widget.onSubmitted, + ), + ); + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e7a5a6e0bf..1c62c33f8a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -988,7 +988,12 @@ } }, "datePicker": { - "dateTimeFormatTooltip": "Change the date and time format in settings" + "dateTimeFormatTooltip": "Change the date and time format in settings", + "dateFormat": "Date format", + "includeTime": "Include time", + "isRange": "End date", + "timeFormat": "Time format", + "clearDate": "Clear date" }, "relativeDates": { "yesterday": "Yesterday",