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 '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/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/board_page.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_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/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/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/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/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/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_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.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/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/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_add_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_header.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_detail.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.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/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_setting_action.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/database_settings_list.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_button.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.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/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.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/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.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:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';

View File

@ -1,25 +1,20 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.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/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/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/common/type_option_separator.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/field_type_option_editor.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'builder.dart'; import 'builder.dart';
import 'date.dart'; import 'date.dart';
class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
final TimestampTypeOptionWidget _widget;
TimestampTypeOptionWidgetBuilder( TimestampTypeOptionWidgetBuilder(
TimestampTypeOptionContext typeOptionContext, TimestampTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex, PopoverMutex popoverMutex,
@ -28,21 +23,22 @@ class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
); );
final TimestampTypeOptionWidget _widget;
@override @override
Widget? build(BuildContext context) { Widget? build(BuildContext context) => _widget;
return _widget;
}
} }
class TimestampTypeOptionWidget extends TypeOptionWidget { class TimestampTypeOptionWidget extends TypeOptionWidget {
final TimestampTypeOptionContext typeOptionContext;
final PopoverMutex popoverMutex;
const TimestampTypeOptionWidget({ const TimestampTypeOptionWidget({
super.key,
required this.typeOptionContext, required this.typeOptionContext,
required this.popoverMutex, required this.popoverMutex,
super.key,
}); });
final TimestampTypeOptionContext typeOptionContext;
final PopoverMutex popoverMutex;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
@ -71,7 +67,7 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
shrinkWrap: true, shrinkWrap: true,
separatorBuilder: (context, index) { separatorBuilder: (context, index) {
if (index == 0) { if (index == 0) {
return const SizedBox(); return const SizedBox.shrink();
} else { } else {
return VSpace(GridSize.typeOptionSeparatorHeight); return VSpace(GridSize.typeOptionSeparatorHeight);
} }
@ -94,17 +90,15 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0), offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) { popupBuilder: (popoverContext) => DateFormatList(
return DateFormatList( selectedFormat: dataFormat,
selectedFormat: dataFormat, onSelected: (format) {
onSelected: (format) { context
context .read<TimestampTypeOptionBloc>()
.read<TimestampTypeOptionBloc>() .add(TimestampTypeOptionEvent.didSelectDateFormat(format));
.add(TimestampTypeOptionEvent.didSelectDateFormat(format)); PopoverContainer.of(popoverContext).close();
PopoverContainer.of(popoverContext).close(); },
}, ),
);
},
child: const Padding( child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0), padding: EdgeInsets.symmetric(horizontal: 12.0),
child: DateFormatButton(), child: DateFormatButton(),
@ -122,17 +116,15 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0), offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) => TimeFormatList(
return TimeFormatList( selectedFormat: timeFormat,
selectedFormat: timeFormat, onSelected: (format) {
onSelected: (format) { context
context .read<TimestampTypeOptionBloc>()
.read<TimestampTypeOptionBloc>() .add(TimestampTypeOptionEvent.didSelectTimeFormat(format));
.add(TimestampTypeOptionEvent.didSelectTimeFormat(format)); PopoverContainer.of(popoverContext).close();
PopoverContainer.of(popoverContext).close(); },
}, ),
);
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: TimeFormatButton(timeFormat: timeFormat), 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:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.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/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_date_picker.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_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.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_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show Either; 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: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'; import 'date_cal_bloc.dart';
class DateCellEditor extends StatefulWidget { class DateCellEditor extends StatefulWidget {
final VoidCallback onDismissed;
final DateCellController cellController;
const DateCellEditor({ const DateCellEditor({
super.key, super.key,
required this.onDismissed, required this.onDismissed,
required this.cellController, required this.cellController,
}); });
final VoidCallback onDismissed;
final DateCellController cellController;
@override @override
State<StatefulWidget> createState() => _DateCellEditor(); State<StatefulWidget> createState() => _DateCellEditor();
} }
@ -41,10 +30,8 @@ class _DateCellEditor extends State<DateCellEditor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<Either<dynamic, FlowyError>>( return FutureBuilder<Either<dynamic, FlowyError>>(
future: widget.cellController.getTypeOption( future: widget.cellController.getTypeOption(DateTypeOptionDataParser()),
DateTypeOptionDataParser(), builder: (_, snapshot) {
),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return _buildWidget(snapshot); return _buildWidget(snapshot);
} }
@ -56,12 +43,10 @@ class _DateCellEditor extends State<DateCellEditor> {
Widget _buildWidget(AsyncSnapshot<Either<dynamic, FlowyError>> snapshot) { Widget _buildWidget(AsyncSnapshot<Either<dynamic, FlowyError>> snapshot) {
return snapshot.data!.fold( return snapshot.data!.fold(
(dateTypeOptionPB) { (dateTypeOptionPB) => _CellCalendarWidget(
return _CellCalendarWidget( cellContext: widget.cellController,
cellContext: widget.cellController, dateTypeOptionPB: dateTypeOptionPB,
dateTypeOptionPB: dateTypeOptionPB, ),
);
},
(err) { (err) {
Log.error(err); Log.error(err);
return const SizedBox.shrink(); return const SizedBox.shrink();
@ -86,6 +71,12 @@ class _CellCalendarWidget extends StatefulWidget {
class _CellCalendarWidgetState extends State<_CellCalendarWidget> { class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
final PopoverMutex popoverMutex = PopoverMutex(); final PopoverMutex popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
@ -94,521 +85,50 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
cellData: widget.cellContext.getCellData(), cellData: widget.cellContext.getCellData(),
cellController: widget.cellContext, cellController: widget.cellContext,
)..add(const DateCellCalendarEvent.initial()), )..add(const DateCellCalendarEvent.initial()),
child: Padding( child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
padding: const EdgeInsets.only(top: 18.0, bottom: 12.0), builder: (context, state) {
child: Column( return AppFlowyDatePicker(
mainAxisSize: MainAxisSize.min, includeTime: state.includeTime,
children: [ onIncludeTimeChanged: (value) => context
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
.read<DateCellCalendarBloc>() .read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setIncludeTime(!value)), .add(DateCellCalendarEvent.setIncludeTime(!value)),
value: includeTime, isRange: state.isRange,
), onIsRangeChanged: (value) => context
);
},
);
}
}
@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
.read<DateCellCalendarBloc>() .read<DateCellCalendarBloc>()
.add(const DateCellCalendarEvent.clearDate()); .add(DateCellCalendarEvent.setIsRange(!value)),
PopoverContainer.of(context).close(); 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/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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';

View File

@ -1,5 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/document/application/doc_service.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/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart'; import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
@ -19,7 +21,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'
Position, Position,
paragraphNode; paragraphNode;
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.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/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.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/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.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/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.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:appflowy_editor/appflowy_editor.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class MentionDateBlock extends StatelessWidget { class MentionDateBlock extends StatefulWidget {
const MentionDateBlock({ const MentionDateBlock({
super.key, super.key,
required this.editorContext, required this.editorContext,
@ -37,11 +42,19 @@ class MentionDateBlock extends StatelessWidget {
final bool includeTime; final bool includeTime;
@override
State<MentionDateBlock> createState() => _MentionDateBlockState();
}
class _MentionDateBlockState extends State<MentionDateBlock> {
late bool includeTime = widget.includeTime;
final PopoverMutex mutex = PopoverMutex();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final editorState = context.read<EditorState>(); final editorState = context.read<EditorState>();
DateTime? parsedDate = DateTime.tryParse(date); DateTime? parsedDate = DateTime.tryParse(widget.date);
if (parsedDate == null) { if (parsedDate == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -62,48 +75,71 @@ class MentionDateBlock extends StatelessWidget {
builder: (context, appearance) => builder: (context, appearance) =>
BlocBuilder<ReminderBloc, ReminderState>( BlocBuilder<ReminderBloc, ReminderState>(
builder: (context, state) { builder: (context, state) {
final reminder = final reminder = state.reminders
state.reminders.firstWhereOrNull((r) => r.id == reminderId); .firstWhereOrNull((r) => r.id == widget.reminderId);
final noReminder = reminder == null && isReminder; final noReminder = reminder == null && widget.isReminder;
final formattedDate = appearance.dateFormat final formattedDate = appearance.dateFormat
.formatDate(parsedDate!, includeTime, appearance.timeFormat); .formatDate(parsedDate!, includeTime, appearance.timeFormat);
final timeStr = parsedDate != null
? _timeFromDate(parsedDate!, appearance.timeFormat)
: null;
final options = DatePickerOptions( final options = DatePickerOptions(
selectedDay: parsedDate,
focusedDay: parsedDate, focusedDay: parsedDate,
firstDay: isReminder popoverMutex: mutex,
selectedDay: parsedDate,
firstDay: widget.isReminder
? noReminder ? noReminder
? parsedDate ? parsedDate
: DateTime.now() : DateTime.now()
: null, : null,
lastDay: noReminder ? parsedDate : null, lastDay: noReminder ? parsedDate : null,
timeStr: timeStr,
includeTime: includeTime, includeTime: includeTime,
enableRanges: false,
dateFormat: appearance.dateFormat,
timeFormat: appearance.timeFormat, timeFormat: appearance.timeFormat,
onIncludeTimeChanged: (includeTime) { onIncludeTimeChanged: (includeTime) {
this.includeTime = includeTime;
_updateBlock(parsedDate!.withoutTime, includeTime); _updateBlock(parsedDate!.withoutTime, includeTime);
// We can remove time from the date/reminder // We can remove time from the date/reminder
// block when toggled off. // block when toggled off.
if (isReminder) { if (widget.isReminder) {
_updateScheduledAt( _updateScheduledAt(
reminderId: reminderId!, reminderId: widget.reminderId!,
selectedDay: selectedDay:
includeTime ? parsedDate! : parsedDate!.withoutTime, includeTime ? parsedDate! : parsedDate!.withoutTime,
includeTime: includeTime, includeTime: includeTime,
); );
} }
}, },
onDaySelected: (selectedDay, focusedDay, includeTime) { onStartTimeChanged: (time) {
parsedDate = selectedDay; 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); _updateBlock(selectedDay, includeTime);
if (isReminder && date != selectedDay.toIso8601String()) { if (widget.isReminder &&
widget.date != selectedDay.toIso8601String()) {
_updateScheduledAt( _updateScheduledAt(
reminderId: reminderId!, reminderId: widget.reminderId!,
selectedDay: selectedDay, selectedDay: selectedDay,
includeTime: includeTime,
); );
} }
}, },
@ -124,9 +160,11 @@ class MentionDateBlock extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
FlowySvg( FlowySvg(
isReminder ? FlowySvgs.clock_alarm_s : FlowySvgs.date_s, widget.isReminder
? FlowySvgs.clock_alarm_s
: FlowySvgs.date_s,
size: const Size.square(18.0), size: const Size.square(18.0),
color: isReminder && reminder?.isAck == true color: widget.isReminder && reminder?.isAck == true
? Theme.of(context).colorScheme.error ? Theme.of(context).colorScheme.error
: null, : null,
), ),
@ -134,7 +172,7 @@ class MentionDateBlock extends StatelessWidget {
FlowyText( FlowyText(
formattedDate, formattedDate,
fontSize: fontSize, fontSize: fontSize,
color: isReminder && reminder?.isAck == true color: widget.isReminder && reminder?.isAck == true
? Theme.of(context).colorScheme.error ? Theme.of(context).colorScheme.error
: null, : 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( void _updateBlock(
DateTime date, [ DateTime date, [
bool includeTime = false, bool includeTime = false,
]) { ]) {
final editorState = editorContext.read<EditorState>(); final editorState = widget.editorContext.read<EditorState>();
final transaction = editorState.transaction final transaction = editorState.transaction
..formatText(node, index, 1, { ..formatText(widget.node, widget.index, 1, {
MentionBlockKeys.mention: { MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionBlockKeys.type: widget.isReminder
isReminder ? MentionType.reminder.name : MentionType.date.name, ? MentionType.reminder.name
: MentionType.date.name,
MentionBlockKeys.date: date.toIso8601String(), MentionBlockKeys.date: date.toIso8601String(),
MentionBlockKeys.uid: reminderId, MentionBlockKeys.uid: widget.reminderId,
MentionBlockKeys.includeTime: includeTime, MentionBlockKeys.includeTime: includeTime,
}, },
}); });
@ -180,7 +241,7 @@ class MentionDateBlock extends StatelessWidget {
required DateTime selectedDay, required DateTime selectedDay,
bool? includeTime, bool? includeTime,
}) { }) {
editorContext.read<ReminderBloc>().add( widget.editorContext.read<ReminderBloc>().add(
ReminderEvent.update( ReminderEvent.update(
ReminderUpdate( ReminderUpdate(
id: reminderId, 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_popover/appflowy_popover.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:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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] /// a [DatePickerMenu]
/// ///
class DatePickerOptions { class DatePickerOptions {
DatePickerOptions({ DatePickerOptions({
DateTime? focusedDay, DateTime? focusedDay,
this.popoverMutex,
this.selectedDay, this.selectedDay,
this.firstDay, this.firstDay,
this.lastDay, this.lastDay,
this.timeStr,
this.includeTime = false, this.includeTime = false,
this.isRange = false,
this.enableRanges = true,
this.dateFormat = UserDateFormatPB.Friendly,
this.timeFormat = UserTimeFormatPB.TwentyFourHour, this.timeFormat = UserTimeFormatPB.TwentyFourHour,
this.onDaySelected, this.onDaySelected,
this.onIncludeTimeChanged, this.onIncludeTimeChanged,
this.onFormatChanged, this.onStartTimeChanged,
this.onPageChanged, this.onEndTimeChanged,
this.onTimeChanged,
}) : focusedDay = focusedDay ?? DateTime.now(); }) : focusedDay = focusedDay ?? DateTime.now();
final DateTime focusedDay; final DateTime focusedDay;
final PopoverMutex? popoverMutex;
final DateTime? selectedDay; final DateTime? selectedDay;
final DateTime? firstDay; final DateTime? firstDay;
final DateTime? lastDay; final DateTime? lastDay;
final String? timeStr;
final bool includeTime; final bool includeTime;
final bool isRange;
final bool enableRanges;
final UserDateFormatPB dateFormat;
final UserTimeFormatPB timeFormat; final UserTimeFormatPB timeFormat;
final DaySelectedCallback? onDaySelected; final DaySelectedCallback? onDaySelected;
final IncludeTimeChangedCallback? onIncludeTimeChanged; final IncludeTimeChangedCallback? onIncludeTimeChanged;
final FormatChangedCallback? onFormatChanged; final TimeChangedCallback? onStartTimeChanged;
final PageChangedCallback? onPageChanged; final TimeChangedCallback? onEndTimeChanged;
final TimeChangedCallback? onTimeChanged;
} }
abstract class DatePickerService { abstract class DatePickerService {
@ -43,8 +56,8 @@ abstract class DatePickerService {
} }
const double _datePickerWidth = 260; const double _datePickerWidth = 260;
const double _datePickerHeight = 325; const double _datePickerHeight = 355;
const double _includeTimeHeight = 60; const double _includeTimeHeight = 40;
const double _ySpacing = 15; const double _ySpacing = 15;
class DatePickerMenu extends DatePickerService { class DatePickerMenu extends DatePickerService {
@ -175,22 +188,27 @@ class _AnimatedDatePickerState extends State<_AnimatedDatePicker> {
constraints: BoxConstraints.loose( constraints: BoxConstraints.loose(
const Size(_datePickerWidth, 465), 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(), focusedDay: widget.options?.focusedDay ?? DateTime.now(),
selectedDate: widget.options?.selectedDay,
firstDay: widget.options?.firstDay, firstDay: widget.options?.firstDay,
lastDay: widget.options?.lastDay, 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/workspace/presentation/widgets/date_picker/utils/layout.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:flutter/material.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({ const IncludeTimeButton({
super.key, super.key,
this.initialTime, required this.value,
required this.popoverMutex, required this.onChanged,
this.includeTime = false,
this.onChanged,
this.onSubmitted,
this.timeFormat = UserTimeFormatPB.TwentyFourHour,
}); });
final String? initialTime; final bool value;
final PopoverMutex? popoverMutex; final Function(bool value) onChanged;
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;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return SizedBox(
children: [ height: DatePickerSize.itemHeight,
if (_includeTime) ...[ child: Padding(
_TimeTextField( padding: DatePickerSize.itemOptionInsets,
timeStr: _timeString, child: Row(
popoverMutex: widget.popoverMutex, children: [
timeFormat: widget.timeFormat, FlowySvg(
onSubmitted: (value) { FlowySvgs.clock_alarm_s,
setState(() => _timeString = value); color: Theme.of(context).iconTheme.color,
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,
),
],
),
), ),
), 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": { "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": { "relativeDates": {
"yesterday": "Yesterday", "yesterday": "Yesterday",