feat: reminder on date (#4288)

* feat: support reminder on date

* feat: support reminder on date in database

* fix: include time static

* fix: do not force unwrap

* chore: clean flutter code

* test: add test for reminder in database

* fix: interpret reminder option

* feat: date and reminder on mobile

* feat: improve notification actions and support open row

* feat: support dates in document

* fix: minor changes + review

* feat: support reminder on mobile in document

* feat: support open row on database reminder mobile

* test: add more tests

* fix: first part of review

* fix: open row responsibility

* fix: abstract application logic from presentation layer

* fix: update reminder on date cell update

* test: fix failing test

* fix: show correct selected day after end date toggled
This commit is contained in:
Mathias Mogensen
2024-01-24 15:15:57 +01:00
committed by GitHub
parent 8105da1c2b
commit baa7c8d826
62 changed files with 2556 additions and 1315 deletions

View File

@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import '../cell_builder.dart';
@ -52,14 +52,15 @@ abstract mixin class GridCellAccessoryState {
}
class PrimaryCellAccessory extends StatefulWidget {
final VoidCallback onTapCallback;
final bool isCellEditing;
const PrimaryCellAccessory({
super.key,
required this.onTapCallback,
required this.isCellEditing,
super.key,
});
final VoidCallback onTapCallback;
final bool isCellEditing;
@override
State<StatefulWidget> createState() => _PrimaryCellAccessoryState();
}

View File

@ -1,14 +1,20 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy_editor/appflowy_editor.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:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
import 'date_cell_bloc.dart';
import 'date_editor.dart';
@ -85,22 +91,32 @@ class _DateCellState extends GridCellState<GridDateCell> {
child: Container(
alignment: alignment,
padding: padding,
child: FlowyText.medium(
text,
color: color,
overflow: TextOverflow.ellipsis,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
text,
color: color,
overflow: TextOverflow.ellipsis,
),
),
if (state.data?.reminderId.isNotEmpty == true) ...[
const HSpace(5),
FlowyTooltip(
message:
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
],
],
),
),
popupBuilder: (BuildContext popoverContent) {
return DateCellEditor(
cellController: _cellController,
onDismissed: () =>
widget.cellContainerNotifier.isFocus = false,
);
},
onClose: () {
widget.cellContainerNotifier.isFocus = false;
},
popupBuilder: (_) => DateCellEditor(
cellController: _cellController,
onDismissed: () => widget.cellContainerNotifier.isFocus = false,
),
onClose: () => widget.cellContainerNotifier.isFocus = false,
);
} else if (widget.cellStyle.useRoundedBorder) {
return InkWell(
@ -108,12 +124,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileDateCellEditScreen(
controller: _cellController,
showAsFullScreen: false,
);
},
builder: (_) => MobileDateCellEditScreen(
controller: _cellController,
showAsFullScreen: false,
),
),
child: Container(
constraints: const BoxConstraints(
@ -146,28 +160,36 @@ class _DateCellState extends GridCellState<GridDateCell> {
scrollDirection: Axis.horizontal,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: FlowyText(
text,
color: color,
fontSize: 15,
maxLines: 1,
child: Row(
children: [
if (state.data?.reminderId.isNotEmpty == true) ...[
FlowyTooltip(
message:
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
const HSpace(5),
],
FlowyText(
text,
color: color,
fontSize: 15,
maxLines: 1,
),
],
),
),
),
onTap: () {
showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
builder: (context) {
return MobileDateCellEditScreen(
controller: _cellController,
showAsFullScreen: false,
);
},
);
},
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
builder: (_) => MobileDateCellEditScreen(
controller: _cellController,
showAsFullScreen: false,
),
),
);
}
},

View File

@ -5,15 +5,22 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
import 'package:appflowy/plugins/database/application/cell/date_cell_service.dart';
import 'package:appflowy/plugins/database/application/field/field_service.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/util/int64_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.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/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'
show StringTranslateExtension;
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:nanoid/non_secure.dart';
import 'package:protobuf/protobuf.dart';
part 'date_cell_editor_bloc.freezed.dart';
@ -22,16 +29,19 @@ class DateCellEditorBloc
extends Bloc<DateCellEditorEvent, DateCellEditorState> {
final DateCellBackendService _dateCellBackendService;
final DateCellController cellController;
final ReminderBloc _reminderBloc;
void Function()? _onCellChangedFn;
DateCellEditorBloc({
required this.cellController,
}) : _dateCellBackendService = DateCellBackendService(
required ReminderBloc reminderBloc,
}) : _reminderBloc = reminderBloc,
_dateCellBackendService = DateCellBackendService(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
rowId: cellController.rowId,
),
super(DateCellEditorState.initial(cellController)) {
super(DateCellEditorState.initial(cellController, reminderBloc)) {
on<DateCellEditorEvent>(
(event, emit) async {
await event.when(
@ -42,6 +52,41 @@ class DateCellEditorBloc
dateCellData.isRange == state.isRange && dateCellData.isRange
? dateCellData.endDateTime
: null;
if (dateCellData.dateTime != null &&
(state.reminderId?.isEmpty ?? true) &&
(dateCellData.reminderId?.isNotEmpty ?? false) &&
state.reminderOption != ReminderOption.none) {
// Add Reminder
_reminderBloc.add(
ReminderEvent.addById(
reminderId: dateCellData.reminderId!,
objectId: cellController.viewId,
meta: {ReminderMetaKeys.rowId: cellController.rowId},
scheduledAt: Int64(
dateCellData.dateTime!
.subtract(state.reminderOption.time)
.millisecondsSinceEpoch ~/
1000,
),
),
);
}
if ((dateCellData.reminderId?.isNotEmpty ?? false) &&
dateCellData.dateTime != null) {
// Update Reminder
_reminderBloc.add(
ReminderEvent.update(
ReminderUpdate(
id: state.reminderId!,
scheduledAt: dateCellData.dateTime!
.subtract(state.reminderOption.time),
),
),
);
}
emit(
state.copyWith(
dateTime: dateCellData.dateTime,
@ -54,11 +99,14 @@ class DateCellEditorBloc
endDay: endDay,
dateStr: dateCellData.dateStr,
endDateStr: dateCellData.endDateStr,
reminderId: dateCellData.reminderId,
),
);
},
didReceiveTimeFormatError:
(String? parseTimeError, String? parseEndTimeError) {
didReceiveTimeFormatError: (
String? parseTimeError,
String? parseEndTimeError,
) {
emit(
state.copyWith(
parseTimeError: parseTimeError,
@ -67,17 +115,14 @@ class DateCellEditorBloc
);
},
selectDay: (date) async {
if (state.isRange) {
return;
if (!state.isRange) {
await _updateDateData(date: date);
}
await _updateDateData(date: date);
},
setIncludeTime: (includeTime) async {
await _updateDateData(includeTime: includeTime);
},
setIsRange: (isRange) async {
await _updateDateData(isRange: isRange);
},
setIncludeTime: (includeTime) async =>
await _updateDateData(includeTime: includeTime),
setIsRange: (isRange) async =>
await _updateDateData(isRange: isRange),
setTime: (timeStr) async {
emit(state.copyWith(timeStr: timeStr));
await _updateDateData(timeStr: timeStr);
@ -87,89 +132,88 @@ class DateCellEditorBloc
final (newStart, newEnd) = state.startDay!.isBefore(start!)
? (state.startDay!, start)
: (start, state.startDay!);
emit(
state.copyWith(
startDay: null,
endDay: null,
),
);
await _updateDateData(
date: newStart.date,
endDate: newEnd.date,
);
emit(state.copyWith(startDay: null, endDay: null));
await _updateDateData(date: newStart.date, endDate: newEnd.date);
} else if (end == null) {
emit(
state.copyWith(
startDay: start,
endDay: null,
),
);
emit(state.copyWith(startDay: start, endDay: null));
} else {
await _updateDateData(
date: start!.date,
endDate: end.date,
);
await _updateDateData(date: start!.date, endDate: end.date);
}
},
setStartDay: (DateTime startDay) async {
if (state.endDay == null) {
emit(
state.copyWith(
startDay: startDay,
),
);
emit(state.copyWith(startDay: startDay));
} else if (startDay.isAfter(state.endDay!)) {
emit(
state.copyWith(
startDay: startDay,
endDay: null,
),
);
emit(state.copyWith(startDay: startDay, endDay: null));
} else {
emit(
state.copyWith(
startDay: startDay,
),
emit(state.copyWith(startDay: startDay));
await _updateDateData(
date: startDay.date,
endDate: state.endDay!.date,
);
_updateDateData(date: startDay.date, endDate: state.endDay!.date);
}
},
setEndDay: (DateTime endDay) async {
setEndDay: (DateTime endDay) {
if (state.startDay == null) {
emit(
state.copyWith(
endDay: endDay,
),
);
emit(state.copyWith(endDay: endDay));
} else if (endDay.isBefore(state.startDay!)) {
emit(
state.copyWith(
startDay: null,
endDay: endDay,
),
);
emit(state.copyWith(startDay: null, endDay: endDay));
} else {
emit(
state.copyWith(
endDay: endDay,
),
);
emit(state.copyWith(endDay: endDay));
_updateDateData(date: state.startDay!.date, endDate: endDay.date);
}
},
setEndTime: (String endTime) async {
setEndTime: (String? endTime) async {
emit(state.copyWith(endTimeStr: endTime));
await _updateDateData(endTimeStr: endTime);
},
setDateFormat: (dateFormat) async {
await _updateTypeOption(emit, dateFormat: dateFormat);
},
setTimeFormat: (timeFormat) async {
await _updateTypeOption(emit, timeFormat: timeFormat);
},
setDateFormat: (DateFormatPB dateFormat) async =>
await _updateTypeOption(emit, dateFormat: dateFormat),
setTimeFormat: (TimeFormatPB timeFormat) async =>
await _updateTypeOption(emit, timeFormat: timeFormat),
clearDate: () async {
// Remove reminder if neccessary
if (state.reminderId != null) {
_reminderBloc
.add(ReminderEvent.remove(reminderId: state.reminderId!));
}
await _clearDate();
},
setReminderOption: (ReminderOption option) async {
if (state.reminderId?.isEmpty ??
true &&
state.dateTime != null &&
option != ReminderOption.none) {
// New Reminder
final reminderId = nanoid();
await _updateDateData(reminderId: reminderId);
emit(state.copyWith(reminderOption: option));
} else if (option == ReminderOption.none &&
(state.reminderId?.isNotEmpty ?? false)) {
// Remove reminder
_reminderBloc
.add(ReminderEvent.remove(reminderId: state.reminderId!));
await _updateDateData(reminderId: "");
emit(state.copyWith(reminderOption: option));
} else if (state.dateTime != null &&
(state.reminderId?.isNotEmpty ?? false)) {
// Update reminder
_reminderBloc.add(
ReminderEvent.update(
ReminderUpdate(
id: state.reminderId!,
scheduledAt: state.dateTime!.subtract(option.time),
),
),
);
}
},
// Empty String signifies no reminder
removeReminder: () async => await _updateDateData(reminderId: ""),
);
},
);
@ -182,6 +226,7 @@ class DateCellEditorBloc
String? endTimeStr,
bool? includeTime,
bool? isRange,
String? reminderId,
}) async {
// make sure that not both date and time are updated at the same time
assert(
@ -191,21 +236,15 @@ class DateCellEditorBloc
// if not updating the time, use the old time in the state
final String? newTime = timeStr ?? state.timeStr;
DateTime? newDate;
if (timeStr != null && timeStr.isNotEmpty) {
newDate = state.dateTime ?? DateTime.now();
} else {
newDate = _utcToLocalAndAddCurrentTime(date);
}
final DateTime? newDate = timeStr != null && timeStr.isNotEmpty
? state.dateTime ?? DateTime.now()
: _utcToLocalAndAddCurrentTime(date);
// if not updating the time, use the old time in the state
final String? newEndTime = endTimeStr ?? state.endTimeStr;
DateTime? newEndDate;
if (endTimeStr != null && endTimeStr.isNotEmpty) {
newEndDate = state.endDateTime ?? DateTime.now();
} else {
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
}
final DateTime? newEndDate = endTimeStr != null && endTimeStr.isNotEmpty
? state.endDateTime ?? DateTime.now()
: _utcToLocalAndAddCurrentTime(endDate);
final result = await _dateCellBackendService.update(
date: newDate,
@ -214,15 +253,14 @@ class DateCellEditorBloc
endTime: newEndTime,
includeTime: includeTime ?? state.includeTime,
isRange: isRange ?? state.isRange,
reminderId: reminderId ?? state.reminderId,
);
result.fold(
(_) {
if (!isClosed &&
(state.parseEndTimeError != null || state.parseTimeError != null)) {
add(
const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
);
add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null));
}
},
(err) {
@ -231,10 +269,12 @@ class DateCellEditorBloc
if (isClosed) {
return;
}
// to determine which textfield should show error
final (startError, endError) = newDate != null
? (timeFormatPrompt(err), null)
: (null, timeFormatPrompt(err));
add(
DateCellEditorEvent.didReceiveTimeFormatError(
startError,
@ -253,13 +293,9 @@ class DateCellEditorBloc
final result = await _dateCellBackendService.clear();
result.fold(
(_) {
if (isClosed) {
return;
if (!isClosed) {
add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null));
}
add(
const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
);
},
(err) => Log.error(err),
);
@ -304,11 +340,11 @@ class DateCellEditorBloc
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cell) {
onCellChanged: (cell) {
if (!isClosed) {
add(DateCellEditorEvent.didReceiveCellUpdate(cell));
}
}),
},
);
}
@ -335,7 +371,7 @@ class DateCellEditorBloc
);
result.fold(
(l) => emit(
(_) => emit(
state.copyWith(
dateTypeOptionPB: newDateTypeOption,
timeHintText: _timeHintText(newDateTypeOption),
@ -355,6 +391,7 @@ class DateCellEditorEvent with _$DateCellEditorEvent {
const factory DateCellEditorEvent.didReceiveCellUpdate(
DateCellDataPB? data,
) = _DidReceiveCellUpdate;
const factory DateCellEditorEvent.didReceiveTimeFormatError(
String? parseTimeError,
String? parseEndTimeError,
@ -362,27 +399,41 @@ class DateCellEditorEvent with _$DateCellEditorEvent {
// date cell data is modified
const factory DateCellEditorEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellEditorEvent.selectDateRange(
DateTime? start,
DateTime? end,
) = _SelectDateRange;
const factory DateCellEditorEvent.setStartDay(
DateTime startDay,
) = _SetStartDay;
const factory DateCellEditorEvent.setEndDay(
DateTime endDay,
) = _SetEndDay;
const factory DateCellEditorEvent.setTime(String time) = _Time;
const factory DateCellEditorEvent.setEndTime(String endTime) = _EndTime;
const factory DateCellEditorEvent.setTime(String time) = _SetTime;
const factory DateCellEditorEvent.setEndTime(String endTime) = _SetEndTime;
const factory DateCellEditorEvent.setIncludeTime(bool includeTime) =
_IncludeTime;
const factory DateCellEditorEvent.setIsRange(bool isRange) = _IsRange;
const factory DateCellEditorEvent.setIsRange(bool isRange) = _SetIsRange;
const factory DateCellEditorEvent.setReminderOption({
required ReminderOption option,
}) = _SetReminderOption;
const factory DateCellEditorEvent.removeReminder() = _RemoveReminder;
// date field type options are modified
const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) =
_TimeFormat;
_SetTimeFormat;
const factory DateCellEditorEvent.setDateFormat(DateFormatPB dateFormat) =
_DateFormat;
_SetDateFormat;
const factory DateCellEditorEvent.clearDate() = _ClearDate;
}
@ -406,17 +457,36 @@ class DateCellEditorState with _$DateCellEditorState {
required bool isRange,
required String? dateStr,
required String? endDateStr,
required String? reminderId,
// error and hint text
required String? parseTimeError,
required String? parseEndTimeError,
required String timeHintText,
@Default(ReminderOption.none) ReminderOption reminderOption,
}) = _DateCellEditorState;
factory DateCellEditorState.initial(DateCellController controller) {
factory DateCellEditorState.initial(
DateCellController controller,
ReminderBloc reminderBloc,
) {
final typeOption = controller.getTypeOption(DateTypeOptionDataParser());
final cellData = controller.getCellData();
final dateCellData = _dateDataFromCellData(cellData);
ReminderOption reminderOption = ReminderOption.none;
if ((dateCellData.reminderId?.isNotEmpty ?? false) &&
dateCellData.dateTime != null) {
final reminder = reminderBloc.state.reminders
.firstWhereOrNull((r) => r.id == dateCellData.reminderId);
if (reminder != null) {
reminderOption = ReminderOption.fromDateDifference(
dateCellData.dateTime!,
reminder.scheduledAt.toDateTime(),
);
}
}
return DateCellEditorState(
dateTypeOptionPB: typeOption,
startDay: dateCellData.isRange ? dateCellData.dateTime : null,
@ -432,6 +502,8 @@ class DateCellEditorState with _$DateCellEditorState {
parseTimeError: null,
parseEndTimeError: null,
timeHintText: _timeHintText(typeOption),
reminderId: dateCellData.reminderId,
reminderOption: reminderOption,
);
}
}
@ -462,6 +534,7 @@ _DateCellData _dateDataFromCellData(
isRange: false,
dateStr: null,
endDateStr: null,
reminderId: null,
);
}
@ -481,12 +554,14 @@ _DateCellData _dateDataFromCellData(
endTimeStr = cellData.endTime;
}
}
final bool includeTime = cellData.includeTime;
final bool isRange = cellData.isRange;
if (cellData.isRange) {
endDateStr = cellData.endDate;
}
final String dateStr = cellData.date;
return _DateCellData(
@ -498,6 +573,7 @@ _DateCellData _dateDataFromCellData(
isRange: isRange,
dateStr: dateStr,
endDateStr: endDateStr,
reminderId: cellData.reminderId,
);
}
@ -510,6 +586,7 @@ class _DateCellData {
final bool isRange;
final String? dateStr;
final String? endDateStr;
final String? reminderId;
_DateCellData({
required this.dateTime,
@ -520,5 +597,6 @@ class _DateCellData {
required this.isRange,
required this.dateStr,
required this.endDateStr,
required this.reminderId,
});
}

View File

@ -1,7 +1,12 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.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_popover/appflowy_popover.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'date_cell_editor_bloc.dart';
@ -31,20 +36,28 @@ class _DateCellEditor extends State<DateCellEditor> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DateCellEditorBloc(
cellController: widget.cellController,
)..add(const DateCellEditorEvent.initial()),
return MultiBlocProvider(
providers: [
BlocProvider<DateCellEditorBloc>(
create: (context) => DateCellEditorBloc(
reminderBloc: getIt<ReminderBloc>(),
cellController: widget.cellController,
)..add(const DateCellEditorEvent.initial()),
),
],
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
final bloc = context.read<DateCellEditorBloc>();
final dateCellBloc = context.read<DateCellEditorBloc>();
return AppFlowyDatePicker(
includeTime: state.includeTime,
rebuildOnDaySelected: false,
onIncludeTimeChanged: (value) =>
bloc.add(DateCellEditorEvent.setIncludeTime(!value)),
dateCellBloc.add(DateCellEditorEvent.setIncludeTime(!value)),
isRange: state.isRange,
startDay: state.isRange ? state.startDay : null,
endDay: state.isRange ? state.endDay : null,
onIsRangeChanged: (value) =>
bloc.add(DateCellEditorEvent.setIsRange(!value)),
dateCellBloc.add(DateCellEditorEvent.setIsRange(!value)),
dateFormat: state.dateTypeOptionPB.dateFormat,
timeFormat: state.dateTypeOptionPB.timeFormat,
selectedDay: state.dateTime,
@ -54,28 +67,36 @@ class _DateCellEditor extends State<DateCellEditor> {
parseEndTimeError: state.parseEndTimeError,
parseTimeError: state.parseTimeError,
popoverMutex: popoverMutex,
onStartTimeSubmitted: (timeStr) {
bloc.add(DateCellEditorEvent.setTime(timeStr));
},
onEndTimeSubmitted: (timeStr) {
bloc.add(DateCellEditorEvent.setEndTime(timeStr));
},
onDaySelected: (selectedDay, _) {
bloc.add(DateCellEditorEvent.selectDay(selectedDay));
},
onRangeSelected: (start, end, _) {
bloc.add(DateCellEditorEvent.selectDateRange(start, end));
},
allowFormatChanges: true,
onDateFormatChanged: (format) {
bloc.add(DateCellEditorEvent.setDateFormat(format));
},
onTimeFormatChanged: (format) {
bloc.add(DateCellEditorEvent.setTimeFormat(format));
},
onClearDate: () {
bloc.add(const DateCellEditorEvent.clearDate());
},
onReminderSelected: (option) => dateCellBloc
.add(DateCellEditorEvent.setReminderOption(option: option)),
selectedReminderOption: state.reminderOption,
options: [
OptionGroup(
options: [
DateTypeOptionButton(
popoverMutex: popoverMutex,
dateFormat: state.dateTypeOptionPB.dateFormat,
timeFormat: state.dateTypeOptionPB.timeFormat,
onDateFormatChanged: (format) => dateCellBloc
.add(DateCellEditorEvent.setDateFormat(format)),
onTimeFormatChanged: (format) => dateCellBloc
.add(DateCellEditorEvent.setTimeFormat(format)),
),
ClearDateButton(
onClearDate: () =>
dateCellBloc.add(const DateCellEditorEvent.clearDate()),
),
],
),
],
onStartTimeSubmitted: (timeStr) =>
dateCellBloc.add(DateCellEditorEvent.setTime(timeStr)),
onEndTimeSubmitted: (timeStr) =>
dateCellBloc.add(DateCellEditorEvent.setEndTime(timeStr)),
onDaySelected: (selectedDay, _) =>
dateCellBloc.add(DateCellEditorEvent.selectDay(selectedDay)),
onRangeSelected: (start, end, _) => dateCellBloc
.add(DateCellEditorEvent.selectDateRange(start, end)),
);
},
),

View File

@ -1,191 +0,0 @@
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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart';
import 'date_cell_editor_bloc.dart';
class MobileDatePicker extends StatefulWidget {
const MobileDatePicker({
super.key,
});
@override
State<MobileDatePicker> createState() => _MobileDatePickerState();
}
class _MobileDatePickerState extends State<MobileDatePicker> {
DateTime _focusedDay = DateTime.now();
CalendarFormat _calendarFormat = CalendarFormat.month;
final ValueNotifier<(DateTime, dynamic)> _currentDateNotifier = ValueNotifier(
(DateTime.now(), null),
);
PageController? _pageController;
@override
Widget build(BuildContext context) {
return Column(
children: [
const VSpace(8.0),
_buildHeader(context),
const VSpace(8.0),
_buildCalendar(context),
const VSpace(16.0),
],
);
}
Widget _buildCalendar(BuildContext context) {
const selectedColor = Color(0xFF00BCF0);
final textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith();
const boxDecoration = BoxDecoration(
shape: BoxShape.circle,
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
return TableCalendar(
firstDay: kFirstDay,
lastDay: kLastDay,
focusedDay: _focusedDay,
rowHeight: 48.0,
calendarFormat: _calendarFormat,
daysOfWeekHeight: 48.0,
rangeSelectionMode: state.isRange
? RangeSelectionMode.enforced
: RangeSelectionMode.disabled,
rangeStartDay: state.isRange ? state.startDay : null,
rangeEndDay: state.isRange ? state.endDay : null,
onCalendarCreated: (pageController) =>
_pageController = pageController,
headerVisible: false,
availableGestures: AvailableGestures.horizontalSwipe,
calendarStyle: CalendarStyle(
cellMargin: const EdgeInsets.all(3.5),
defaultDecoration: boxDecoration,
selectedDecoration: boxDecoration.copyWith(
color: selectedColor,
),
todayDecoration: boxDecoration.copyWith(
color: Colors.transparent,
border: Border.all(color: selectedColor),
),
weekendDecoration: boxDecoration,
outsideDecoration: boxDecoration,
rangeStartDecoration: boxDecoration.copyWith(
color: selectedColor,
),
rangeEndDecoration: boxDecoration.copyWith(
color: selectedColor,
),
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: textStyle.copyWith(
color: Theme.of(context).hintColor,
fontSize: 14.0,
),
),
),
);
},
),
selectedDayPredicate: (day) =>
state.isRange ? false : isSameDay(state.dateTime, day),
onDaySelected: (selectedDay, focusedDay) {
context.read<DateCellEditorBloc>().add(
DateCellEditorEvent.selectDay(selectedDay),
);
},
onRangeSelected: (start, end, focusedDay) {
context.read<DateCellEditorBloc>().add(
DateCellEditorEvent.selectDateRange(start, end),
);
},
onFormatChanged: (calendarFormat) => setState(() {
_calendarFormat = calendarFormat;
}),
onPageChanged: (focusedDay) => setState(() {
_focusedDay = focusedDay;
_currentDateNotifier.value = (focusedDay, null);
}),
);
},
),
);
}
Widget _buildHeader(BuildContext context) {
return Row(
children: [
const HSpace(16.0),
ValueListenableBuilder(
valueListenable: _currentDateNotifier,
builder: (_, value, ___) {
return FlowyText(
DateFormat.yMMMM(value.$2).format(value.$1),
);
},
),
const Spacer(),
FlowyButton(
useIntrinsicWidth: true,
text: FlowySvg(
FlowySvgs.arrow_left_s,
color: Theme.of(context).iconTheme.color,
size: const Size.square(24.0),
),
onTap: () => _pageController?.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
),
),
const HSpace(24.0),
FlowyButton(
useIntrinsicWidth: true,
text: FlowySvg(
FlowySvgs.arrow_right_s,
color: Theme.of(context).iconTheme.color,
size: const Size.square(24.0),
),
onTap: () => _pageController?.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
),
),
const HSpace(8.0),
],
);
}
}