mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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)),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user