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