refactor: date picker abstraction (#4159)

* refactor: date picker abstraction

* refactor: move include time button
This commit is contained in:
Mathias Mogensen 2023-12-18 13:10:12 +01:00 committed by GitHub
parent 0783f94cd6
commit 7d512578b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1082 additions and 1172 deletions

View File

@ -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';

View File

@ -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<TimestampTypeOptionBloc>()
.add(TimestampTypeOptionEvent.didSelectDateFormat(format));
PopoverContainer.of(popoverContext).close();
},
);
},
popupBuilder: (popoverContext) => DateFormatList(
selectedFormat: dataFormat,
onSelected: (format) {
context
.read<TimestampTypeOptionBloc>()
.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<TimestampTypeOptionBloc>()
.add(TimestampTypeOptionEvent.didSelectTimeFormat(format));
PopoverContainer.of(popoverContext).close();
},
);
},
popupBuilder: (BuildContext popoverContext) => TimeFormatList(
selectedFormat: timeFormat,
onSelected: (format) {
context
.read<TimestampTypeOptionBloc>()
.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,
),
],
),
),
);
}
}

View File

@ -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<StatefulWidget> createState() => _DateCellEditor();
}
@ -41,10 +30,8 @@ class _DateCellEditor extends State<DateCellEditor> {
@override
Widget build(BuildContext context) {
return FutureBuilder<Either<dynamic, FlowyError>>(
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<DateCellEditor> {
Widget _buildWidget(AsyncSnapshot<Either<dynamic, FlowyError>> 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<DateCellCalendarBloc, DateCellCalendarState>(
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<DateCellCalendarBloc, DateCellCalendarState>(
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<DatePicker> createState() => _DatePickerState();
}
class _DatePickerState extends State<DatePicker> {
DateTime _focusedDay = DateTime.now();
CalendarFormat _calendarFormat = CalendarFormat.month;
@override
Widget build(BuildContext context) {
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
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<DateCellCalendarBloc>().add(
DateCellCalendarEvent.selectDay(selectedDay),
);
},
onRangeSelected: (start, end, focusedDay) {
context.read<DateCellCalendarBloc>().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<DateCellCalendarBloc, DateCellCalendarState, bool>(
selector: (state) => state.includeTime,
builder: (context, includeTime) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: IncludeTimeButton(
onChanged: (value) => context
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
builder: (context, state) {
return AppFlowyDatePicker(
includeTime: state.includeTime,
onIncludeTimeChanged: (value) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setIncludeTime(!value)),
value: includeTime,
),
);
},
);
}
}
@visibleForTesting
class EndTimeButton extends StatelessWidget {
const EndTimeButton({super.key});
@override
Widget build(BuildContext context) {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
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<DateCellCalendarBloc>()
.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<DateCellCalendarBloc, DateCellCalendarState>(
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<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setEndTime(timeStr));
} else {
context
.read<DateCellCalendarBloc>()
.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<DateCellCalendarBloc, DateCellCalendarState,
DateTypeOptionPB>(
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<DateCellCalendarBloc>().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<Widget> 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<DateCellCalendarBloc>()
.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<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setTime(timeStr)),
onEndTimeSubmitted: (timeStr) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setEndTime(timeStr)),
onDaySelected: (selectedDay, _) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.selectDay(selectedDay)),
onRangeSelected: (start, end, _) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.selectDateRange(start, end)),
allowFormatChanges: true,
onDateFormatChanged: (format) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setDateFormat(format)),
onTimeFormatChanged: (format) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setTimeFormat(format)),
onClearDate: () => context
.read<DateCellCalendarBloc>()
.add(const DateCellCalendarEvent.clearDate()),
);
},
),
);
}

View File

@ -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';

View File

@ -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';

View File

@ -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<MentionDateBlock> createState() => _MentionDateBlockState();
}
class _MentionDateBlockState extends State<MentionDateBlock> {
late bool includeTime = widget.includeTime;
final PopoverMutex mutex = PopoverMutex();
@override
Widget build(BuildContext context) {
final editorState = context.read<EditorState>();
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<ReminderBloc, ReminderState>(
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<EditorState>();
final editorState = widget.editorContext.read<EditorState>();
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<ReminderBloc>().add(
widget.editorContext.read<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: reminderId,

View File

@ -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<AppFlowyCalendar> createState() => _AppFlowyCalendarState();
}
class _AppFlowyCalendarState extends State<AppFlowyCalendar>
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;
}

View File

@ -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<AppFlowyDatePicker> createState() => _AppFlowyDatePickerState();
}
class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
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!),
],
],
),
);
}
}

View File

@ -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,
};
}

View File

@ -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);
}

View File

@ -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,
};
}

View File

@ -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();
},
),
),
);
}
}

View File

@ -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<DatePicker> createState() => _DatePickerState();
}
class _DatePickerState extends State<DatePicker> {
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,
),
);
}
}

View File

@ -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,
),
),
);

View File

@ -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<DateTimeSetting> createState() => _DateTimeSettingState();
}
class _DateTimeSettingState extends State<DateTimeSetting> {
final timeSettingPopoverMutex = PopoverMutex();
String? overlayIdentifier;
@override
Widget build(BuildContext context) {
final List<Widget> 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();
}
}

View File

@ -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();
},
),
);
}
}

View File

@ -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(),
);
}
}

View File

@ -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,
),
],
),
),
),
);
}
}

View File

@ -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<IncludeTimeButton> createState() => _IncludeTimeButtonState();
}
class _IncludeTimeButtonState extends State<IncludeTimeButton> {
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(),
_ => "",
};
}

View File

@ -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(),
);
}
}

View File

@ -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<TimeTextField> createState() => _TimeTextFieldState();
}
class _TimeTextFieldState extends State<TimeTextField> {
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,
),
);
}
}

View File

@ -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",