mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
refactor: date picker abstraction (#4159)
* refactor: date picker abstraction * refactor: move include time button
This commit is contained in:
parent
0783f94cd6
commit
7d512578b2
@ -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';
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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()),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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!),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
|
||||||
_ => "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user