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

@ -0,0 +1,122 @@
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('reminder in database', () {
testWidgets('add date field and add reminder', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditPropertyButton();
// Change to date type
await tester.tapTypeOptionButton();
await tester.selectFieldType(FieldType.DateTime);
await tester.dismissFieldEditor();
// Open date picker
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.findDateEditor(findsOneWidget);
// Select date
await tester.selectLastDateInPicker();
// Select Time of event reminder
await tester.selectReminderOption(ReminderOption.atTimeOfEvent);
// Expect Time of event to be displayed
tester.expectSelectedReminder(ReminderOption.atTimeOfEvent);
// Dismiss the cell/date editor
await tester.dismissCellEditor();
// Open date picker again
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.findDateEditor(findsOneWidget);
// Expect Time of event to be displayed
tester.expectSelectedReminder(ReminderOption.atTimeOfEvent);
// Dismiss the cell/date editor
await tester.dismissCellEditor();
// Open "Upcoming" in Notification hub
await tester.openNotificationHub(tabIndex: 1);
// Expect 1 notification
tester.expectNotificationItems(1);
});
testWidgets('navigate from reminder to open row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditPropertyButton();
// Change to date type
await tester.tapTypeOptionButton();
await tester.selectFieldType(FieldType.DateTime);
await tester.dismissFieldEditor();
// Open date picker
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.findDateEditor(findsOneWidget);
// Select date
await tester.selectLastDateInPicker();
// Select Time of event reminder
await tester.selectReminderOption(ReminderOption.atTimeOfEvent);
// Expect Time of event to be displayed
tester.expectSelectedReminder(ReminderOption.atTimeOfEvent);
// Dismiss the cell/date editor
await tester.dismissCellEditor();
// Open date picker again
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.findDateEditor(findsOneWidget);
// Expect Time of event to be displayed
tester.expectSelectedReminder(ReminderOption.atTimeOfEvent);
// Dismiss the cell/date editor
await tester.dismissCellEditor();
// Create and Navigate to a new document
await tester.createNewPageWithNameUnderParent();
await tester.pumpAndSettle();
// Open "Upcoming" in Notification hub
await tester.openNotificationHub(tabIndex: 1);
// Expect 1 notification
tester.expectNotificationItems(1);
// Tap on the notification
await tester.tap(find.byType(NotificationItem));
await tester.pumpAndSettle();
// Expect to see Row Editor Dialog
tester.expectToSeeRowDetailsPageDialog();
});
});
}

View File

@ -1,20 +1,23 @@
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import '../util/base.dart'; import '../util/base.dart';
import '../util/common_operations.dart'; import '../util/common_operations.dart';
import '../util/editor_test_operations.dart'; import '../util/editor_test_operations.dart';
import '../util/expectation.dart';
import '../util/keyboard.dart'; import '../util/keyboard.dart';
void main() { void main() {
@ -35,7 +38,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Trigger iline action menu and type 'remind tomorrow' // Trigger inline action menu and type 'remind tomorrow'
final tomorrow = await _insertReminderTomorrow(tester); final tomorrow = await _insertReminderTomorrow(tester);
Node node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!; Node node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!;
@ -43,7 +46,7 @@ void main() {
node.delta!.first.attributes![MentionBlockKeys.mention]; node.delta!.first.attributes![MentionBlockKeys.mention];
expect(node.type, 'paragraph'); expect(node.type, 'paragraph');
expect(mentionAttr['type'], MentionType.reminder.name); expect(mentionAttr['type'], MentionType.date.name);
expect(mentionAttr['date'], tomorrow.toIso8601String()); expect(mentionAttr['date'], tomorrow.toIso8601String());
await tester.tap( await tester.tap(
@ -67,9 +70,57 @@ void main() {
_dateWithTime(dateTimeSettings.timeFormat, tomorrow, time); _dateWithTime(dateTimeSettings.timeFormat, tomorrow, time);
expect(node.type, 'paragraph'); expect(node.type, 'paragraph');
expect(mentionAttr['type'], MentionType.reminder.name); expect(mentionAttr['type'], MentionType.date.name);
expect(mentionAttr['date'], tomorrowWithTime.toIso8601String()); expect(mentionAttr['date'], tomorrowWithTime.toIso8601String());
}); });
testWidgets('Add reminder for tomorrow, and navigate to it',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.getCurrentEditorState().insertNewLine();
await tester.pumpAndSettle();
// Trigger inline action menu and type 'remind tomorrow'
final tomorrow = await _insertReminderTomorrow(tester);
final Node node =
tester.editor.getCurrentEditorState().getNodeAtPath([1])!;
final Map<String, dynamic> mentionAttr =
node.delta!.first.attributes![MentionBlockKeys.mention];
expect(node.type, 'paragraph');
expect(mentionAttr['type'], MentionType.date.name);
expect(mentionAttr['date'], tomorrow.toIso8601String());
// Create and Navigate to a new document
await tester.createNewPageWithNameUnderParent();
await tester.pumpAndSettle();
// Open "Upcoming" in Notification hub
await tester.openNotificationHub(tabIndex: 1);
// Expect 1 notification
tester.expectNotificationItems(1);
// Tap on the notification
await tester.tap(find.byType(NotificationItem));
await tester.pumpAndSettle();
// Expect node at path 1 to be the date/reminder
expect(
tester.editor
.getCurrentEditorState()
.getNodeAtPath([1])
?.delta
?.first
.attributes?[MentionBlockKeys.mention]['type'],
MentionType.date.name,
);
});
}); });
} }

View File

@ -14,6 +14,9 @@ import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_it
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -493,6 +496,30 @@ extension CommonOperations on WidgetTester {
await tapEmoji(icon); await tapEmoji(icon);
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> openNotificationHub({
int tabIndex = 0,
}) async {
final finder = find.descendant(
of: find.byType(NotificationButton),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.clock_alarm_s,
),
);
await tap(finder);
await pumpAndSettle();
if (tabIndex == 1) {
final tabFinder = find.descendant(
of: find.byType(NotificationTabBar),
matching: find.byType(FlowyTabItem).at(1),
);
await tap(tabFinder);
await pumpAndSettle();
}
}
} }
extension ViewLayoutPBTest on ViewLayoutPB { extension ViewLayoutPBTest on ViewLayoutPB {

View File

@ -60,6 +60,7 @@ import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.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/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/end_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
@ -76,6 +77,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';
// Non-exported member of the table_calendar library
import 'package:table_calendar/src/widgets/cell_content.dart';
import 'base.dart'; import 'base.dart';
import 'common_operations.dart'; import 'common_operations.dart';
import 'expectation.dart'; import 'expectation.dart';
@ -343,6 +347,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(finder); await tapButton(finder);
} }
Future<void> selectReminderOption(ReminderOption option) async {
await hoverOnWidget(find.byType(ReminderSelector));
final finder = find.descendant(
of: find.byType(FlowyButton),
matching: find.text(option.label),
);
await tapButton(finder);
}
Future<void> selectLastDateInPicker() async {
final finder = find.byType(CellContent).last;
await tapButton(finder);
}
Future<void> toggleDateRange() async { Future<void> toggleDateRange() async {
final findDateEditor = find.byType(EndTimeButton); final findDateEditor = find.byType(EndTimeButton);
final findToggle = find.byType(Toggle); final findToggle = find.byType(Toggle);

View File

@ -1,14 +1,18 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'util.dart'; import 'util.dart';
@ -242,4 +246,29 @@ extension Expectation on WidgetTester {
); );
expect(icon, findsOneWidget); expect(icon, findsOneWidget);
} }
void expectSelectedReminder(ReminderOption option) {
final findSelectedText = find.descendant(
of: find.byType(ReminderSelector),
matching: find.text(option.label),
);
expect(findSelectedText, findsOneWidget);
}
void expectNotificationItems(int amount) {
final findItems = find.byType(NotificationItem);
expect(findItems, findsNWidgets(amount));
}
void expectToSeeRowDetailsPageDialog() {
expect(
find.descendant(
of: find.byType(RowDetailPage),
matching: find.byType(SimpleDialog),
),
findsOneWidget,
);
}
} }

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
@ -8,11 +10,11 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
extension MobileRouter on BuildContext { extension MobileRouter on BuildContext {
Future<void> pushView(ViewPB view) async { Future<void> pushView(ViewPB view, [Map<String, dynamic>? arguments]) async {
push( push(
Uri( Uri(
path: view.routeName, path: view.routeName,
queryParameters: view.queryParameters, queryParameters: view.queryParameters(arguments),
).toString(), ).toString(),
).then((value) { ).then((value) {
RecentService().updateRecentViews([view.id], true); RecentService().updateRecentViews([view.id], true);
@ -36,7 +38,7 @@ extension on ViewPB {
} }
} }
Map<String, dynamic> get queryParameters { Map<String, dynamic> queryParameters([Map<String, dynamic>? arguments]) {
switch (layout) { switch (layout) {
case ViewLayoutPB.Document: case ViewLayoutPB.Document:
return { return {
@ -47,6 +49,7 @@ extension on ViewPB {
return { return {
MobileGridScreen.viewId: id, MobileGridScreen.viewId: id,
MobileGridScreen.viewTitle: name, MobileGridScreen.viewTitle: name,
MobileGridScreen.viewArgs: jsonEncode(arguments),
}; };
case ViewLayoutPB.Calendar: case ViewLayoutPB.Calendar:
return { return {

View File

@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
@ -13,7 +17,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:dartz/dartz.dart' hide State; import 'package:dartz/dartz.dart' hide State;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -21,14 +24,16 @@ class MobileViewPage extends StatefulWidget {
const MobileViewPage({ const MobileViewPage({
super.key, super.key,
required this.id, required this.id,
this.title,
required this.viewLayout, required this.viewLayout,
this.title,
this.arguments,
}); });
/// view id /// view id
final String id; final String id;
final String? title;
final ViewLayoutPB viewLayout; final ViewLayoutPB viewLayout;
final String? title;
final Map<String, dynamic>? arguments;
@override @override
State<MobileViewPage> createState() => _MobileViewPageState(); State<MobileViewPage> createState() => _MobileViewPageState();
@ -40,7 +45,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
future = ViewBackendService.getView(widget.id); future = ViewBackendService.getView(widget.id);
} }
@ -67,7 +71,10 @@ class _MobileViewPageState extends State<MobileViewPage> {
body = state.data!.fold((view) { body = state.data!.fold((view) {
viewPB = view; viewPB = view;
actions.add(_buildAppBarMoreButton(view)); actions.add(_buildAppBarMoreButton(view));
return view.plugin().widgetBuilder.buildWidget(shrinkWrap: false); return view
.plugin(arguments: widget.arguments ?? const {})
.widgetBuilder
.buildWidget(shrinkWrap: false);
}, (error) { }, (error) {
return FlowyMobileStateContainer.error( return FlowyMobileStateContainer.error(
emoji: '😔', emoji: '😔',
@ -89,6 +96,10 @@ class _MobileViewPageState extends State<MobileViewPage> {
create: (_) => create: (_) =>
ViewBloc(view: viewPB!)..add(const ViewEvent.initial()), ViewBloc(view: viewPB!)..add(const ViewEvent.initial()),
), ),
BlocProvider.value(
value: getIt<ReminderBloc>()
..add(const ReminderEvent.started()),
),
], ],
child: Builder( child: Builder(
builder: (context) { builder: (context) {
@ -131,9 +142,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
leading: const AppBarBackButton(), leading: const AppBarBackButton(),
actions: actions, actions: actions,
), ),
body: SafeArea( body: SafeArea(child: child),
child: child,
),
); );
} }

View File

@ -1,24 +1,24 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.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/widgets/widgets.dart';
import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_editor_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_editor_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/mobile_date_editor.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileDateCellEditScreen extends StatefulWidget { class MobileDateCellEditScreen extends StatefulWidget {
static const routeName = '/edit_date_cell'; static const routeName = '/edit_date_cell';
// the type is DateCellController // the type is DateCellController
static const dateCellController = 'date_cell_controller'; static const dateCellController = 'date_cell_controller';
// bool value, default is true // bool value, default is true
static const fullScreen = 'full_screen'; static const fullScreen = 'full_screen';
@ -38,20 +38,13 @@ class MobileDateCellEditScreen extends StatefulWidget {
class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> { class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) =>
return widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen(); widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen();
}
Widget _buildFullScreen() { Widget _buildFullScreen() {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: FlowyText.medium(LocaleKeys.titleBar_date.tr())),
title: FlowyText.medium( body: _buildDatePicker(),
LocaleKeys.titleBar_date.tr(),
),
),
body: _DateCellEditBody(
dateCellController: widget.controller,
),
); );
} }
@ -71,353 +64,73 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: const Center(child: DragHandler()), child: const Center(child: DragHandler()),
), ),
_buildHeader(), const MobileDateHeader(),
_DateCellEditBody( _buildDatePicker(),
dateCellController: widget.controller,
),
], ],
), ),
), ),
); );
} }
Widget _buildHeader() { Widget _buildDatePicker() => MultiBlocProvider(
const iconWidth = 30.0; providers: [
const height = 44.0; BlocProvider<DateCellEditorBloc>(
return Container( create: (_) => DateCellEditorBloc(
color: Theme.of(context).colorScheme.surface, reminderBloc: getIt<ReminderBloc>(),
padding: const EdgeInsets.symmetric(horizontal: 8.0), cellController: widget.controller,
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(iconWidth),
),
width: iconWidth,
iconPadding: EdgeInsets.zero,
onPressed: () => context.pop(),
),
),
Align(
alignment: Alignment.center,
child: FlowyText.medium(
LocaleKeys.grid_field_dateFieldName.tr(),
fontSize: 16,
),
),
].map((e) => SizedBox(height: height, child: e)).toList(),
),
);
}
}
class _DateCellEditBody extends StatelessWidget {
const _DateCellEditBody({
required this.dateCellController,
});
final DateCellController dateCellController;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DateCellEditorBloc(
cellController: dateCellController,
)..add(const DateCellEditorEvent.initial()), )..add(const DateCellEditorEvent.initial()),
child: const Column(
children: [
FlowyOptionDecorateBox(
showTopBorder: false,
child: _IncludeTimePicker(),
),
_Divider(),
FlowyOptionDecorateBox(
child: MobileDatePicker(),
),
_Divider(),
_EndDateSwitch(),
_IncludeTimeSwitch(),
_Divider(),
_ClearDateButton(),
_Divider(),
],
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider();
@override
Widget build(BuildContext context) {
return const VSpace(20.0);
}
}
class _IncludeTimePicker extends StatefulWidget {
const _IncludeTimePicker();
@override
State<_IncludeTimePicker> createState() => _IncludeTimePickerState();
}
class _IncludeTimePickerState extends State<_IncludeTimePicker> {
String? _selectedTime;
@override
Widget build(BuildContext context) {
return BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
final startDay = state.dateStr;
final endDay = state.endDateStr;
final includeTime = state.includeTime;
final use24hFormat =
state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwentyFourHour;
if (startDay == null || startDay.isEmpty) {
return const Divider(
height: 1,
);
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTime(
context,
includeTime,
use24hFormat,
true,
startDay,
state.timeStr,
),
VSpace(
8.0,
color: Theme.of(context).colorScheme.surface,
),
_buildTime(
context,
includeTime,
use24hFormat,
false,
endDay,
state.endTimeStr,
), ),
], ],
), child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
);
},
);
}
Widget _buildTime(
BuildContext context,
bool isIncludeTime,
bool use24hFormat,
bool isStartDay,
String? dateStr,
String? timeStr,
) {
if (dateStr == null) {
return const SizedBox.shrink();
}
final List<Widget> children = [];
if (!isIncludeTime) {
children.addAll([
const HSpace(12.0),
FlowyText(
dateStr,
),
]);
} else {
children.addAll([
Expanded(
child: FlowyText(
dateStr,
textAlign: TextAlign.center,
),
),
Container(
width: 1,
height: 16,
color: Colors.grey,
),
Expanded(
child: FlowyText(
timeStr ?? '',
textAlign: TextAlign.center,
),
),
]);
}
return GestureDetector(
onTap: () async {
final bloc = context.read<DateCellEditorBloc>();
await showMobileBottomSheet(
context,
builder: (context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 300,
),
child: CupertinoDatePicker(
showDayOfWeek: false,
mode: CupertinoDatePickerMode.time,
use24hFormat: use24hFormat,
onDateTimeChanged: (dateTime) {
_selectedTime = use24hFormat
? DateFormat('HH:mm').format(dateTime)
: DateFormat('hh:mm a').format(dateTime);
},
),
);
},
);
if (_selectedTime != null) {
bloc.add(
isStartDay
? DateCellEditorEvent.setTime(_selectedTime!)
: DateCellEditorEvent.setEndTime(_selectedTime!),
);
}
},
child: Container(
constraints: const BoxConstraints(
minHeight: 36,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Theme.of(context).colorScheme.secondaryContainer,
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
child: Row(
children: children,
),
),
);
}
}
class _EndDateSwitch extends StatelessWidget {
const _EndDateSwitch();
@override
Widget build(BuildContext context) {
return BlocSelector<DateCellEditorBloc, DateCellEditorState, bool>(
selector: (state) => state.isRange,
builder: (context, isRange) {
return FlowyOptionTile.toggle(
text: LocaleKeys.grid_field_isRange.tr(),
isSelected: isRange,
onValueChanged: (value) {
context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIsRange(value));
},
);
},
);
}
}
class _IncludeTimeSwitch extends StatelessWidget {
const _IncludeTimeSwitch();
@override
Widget build(BuildContext context) {
return BlocSelector<DateCellEditorBloc, DateCellEditorState, bool>(
selector: (state) => state.includeTime,
builder: (context, includeTime) {
return FlowyOptionTile.toggle(
showTopBorder: false,
text: LocaleKeys.grid_field_includeTime.tr(),
isSelected: includeTime,
onValueChanged: (value) {
context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIncludeTime(value));
},
);
},
);
}
}
class _TimeTextField extends StatefulWidget {
const _TimeTextField({
required this.timeStr,
required this.isEndTime,
});
final String? timeStr;
final bool isEndTime;
@override
State<_TimeTextField> createState() => _TimeTextFieldState();
}
class _TimeTextFieldState extends State<_TimeTextField> {
late final TextEditingController _textController =
TextEditingController(text: widget.timeStr);
@override
Widget build(BuildContext context) {
return BlocConsumer<DateCellEditorBloc, DateCellEditorState>(
listener: (context, state) {
_textController.text =
widget.isEndTime ? state.endTimeStr ?? "" : state.timeStr ?? "";
},
builder: (context, state) { builder: (context, state) {
return TextFormField( return MobileAppFlowyDatePicker(
controller: _textController, selectedDay: state.dateTime,
textAlign: TextAlign.end, dateStr: state.dateStr,
decoration: InputDecoration( endDateStr: state.endDateStr,
hintText: state.timeHintText, timeStr: state.timeStr,
errorText: widget.isEndTime endTimeStr: state.endTimeStr,
? state.parseEndTimeError startDay: state.startDay,
: state.parseTimeError, endDay: state.endDay,
), enableRanges: true,
keyboardType: TextInputType.datetime, isRange: state.isRange,
onFieldSubmitted: (timeStr) { includeTime: state.includeTime,
context.read<DateCellEditorBloc>().add( use24hFormat: state.dateTypeOptionPB.timeFormat ==
widget.isEndTime TimeFormatPB.TwentyFourHour,
? DateCellEditorEvent.setEndTime(timeStr) selectedReminderOption: state.reminderOption,
: DateCellEditorEvent.setTime(timeStr), onStartTimeChanged: (String? time) {
); if (time != null) {
}, context
); .read<DateCellEditorBloc>()
}, .add(DateCellEditorEvent.setTime(time));
);
} }
},
@override onEndTimeChanged: (String? time) {
void dispose() { if (time != null) {
_textController.dispose(); context
super.dispose(); .read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setEndTime(time));
} }
} },
onDaySelected: (selectedDay, focusedDay) => context
class _ClearDateButton extends StatelessWidget { .read<DateCellEditorBloc>()
const _ClearDateButton(); .add(DateCellEditorEvent.selectDay(selectedDay)),
onRangeSelected: (start, end, focused) => context
@override .read<DateCellEditorBloc>()
Widget build(BuildContext context) { .add(DateCellEditorEvent.selectDateRange(start, end)),
return FlowyOptionTile.text( onRangeChanged: (value) => context
text: LocaleKeys.grid_field_clearDate.tr(), .read<DateCellEditorBloc>()
onTap: () => context .add(DateCellEditorEvent.setIsRange(value)),
onIncludeTimeChanged: (value) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIncludeTime(value)),
onClearDate: () => context
.read<DateCellEditorBloc>() .read<DateCellEditorBloc>()
.add(const DateCellEditorEvent.clearDate()), .add(const DateCellEditorEvent.clearDate()),
onReminderSelected: (option) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setReminderOption(option: option)),
);
},
),
); );
}
} }

View File

@ -1,28 +1,33 @@
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
class MobileGridScreen extends StatelessWidget { class MobileGridScreen extends StatelessWidget {
static const routeName = '/grid'; static const routeName = '/grid';
static const viewId = 'id'; static const viewId = 'id';
static const viewTitle = 'title'; static const viewTitle = 'title';
static const viewArgs = 'arguments';
const MobileGridScreen({ const MobileGridScreen({
super.key, super.key,
required this.id, required this.id,
this.title, this.title,
this.arguments,
}); });
/// view id /// view id
final String id; final String id;
final String? title; final String? title;
final Map<String, dynamic>? arguments;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MobileViewPage( return MobileViewPage(
id: id, id: id,
title: title, title: title,
viewLayout: ViewLayoutPB.Document, viewLayout: ViewLayoutPB.Grid,
arguments: arguments,
); );
} }
} }

View File

@ -159,7 +159,7 @@ class _NotificationScreenContent extends StatelessWidget {
); );
void _onDelete(ReminderPB reminder) => void _onDelete(ReminderPB reminder) =>
reminderBloc.add(ReminderEvent.remove(reminder: reminder)); reminderBloc.add(ReminderEvent.remove(reminderId: reminder.id));
void _onReadChanged(ReminderPB reminder, bool isRead) => reminderBloc.add( void _onReadChanged(ReminderPB reminder, bool isRead) => reminderBloc.add(
ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)), ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),

View File

@ -18,12 +18,13 @@ final class DateCellBackendService {
..rowId = rowId; ..rowId = rowId;
Future<Either<Unit, FlowyError>> update({ Future<Either<Unit, FlowyError>> update({
required bool includeTime,
required bool isRange,
DateTime? date, DateTime? date,
String? time, String? time,
DateTime? endDate, DateTime? endDate,
String? endTime, String? endTime,
required includeTime, String? reminderId,
required isRange,
}) { }) {
final payload = DateChangesetPB.create() final payload = DateChangesetPB.create()
..cellId = cellId ..cellId = cellId
@ -44,6 +45,9 @@ final class DateCellBackendService {
if (endTime != null) { if (endTime != null) {
payload.endTime = endTime; payload.endTime = endTime;
} }
if (reminderId != null) {
payload.reminderId = reminderId;
}
return DatabaseEventUpdateDateCell(payload).send(); return DatabaseEventUpdateDateCell(payload).send();
} }

View File

@ -1,5 +1,8 @@
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_content.dart'; import 'package:appflowy/mobile/presentation/database/board/mobile_board_content.dart';
@ -21,8 +24,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../widgets/card/card.dart'; import '../../widgets/card/card.dart';
@ -30,6 +31,7 @@ import '../../widgets/card/card_cell_builder.dart';
import '../../widgets/card/cells/card_cell.dart'; import '../../widgets/card/cells/card_cell.dart';
import '../../widgets/row/cell_builder.dart'; import '../../widgets/row/cell_builder.dart';
import '../application/board_bloc.dart'; import '../application/board_bloc.dart';
import 'toolbar/board_setting_bar.dart'; import 'toolbar/board_setting_bar.dart';
import 'widgets/board_hidden_groups.dart'; import 'widgets/board_hidden_groups.dart';
@ -40,6 +42,7 @@ class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
ViewPB view, ViewPB view,
DatabaseController controller, DatabaseController controller,
bool shrinkWrap, bool shrinkWrap,
String? initialRowId,
) => ) =>
BoardPage(view: view, databaseController: controller); BoardPage(view: view, databaseController: controller);

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@ -19,7 +21,6 @@ import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -27,6 +28,7 @@ import '../../application/row/row_cache.dart';
import '../../application/row/row_controller.dart'; import '../../application/row/row_controller.dart';
import '../../widgets/row/cell_builder.dart'; import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart'; import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart'; import 'calendar_day.dart';
import 'layout/sizes.dart'; import 'layout/sizes.dart';
import 'toolbar/calendar_setting_bar.dart'; import 'toolbar/calendar_setting_bar.dart';
@ -38,6 +40,7 @@ class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
ViewPB view, ViewPB view,
DatabaseController controller, DatabaseController controller,
bool shrinkWrap, bool shrinkWrap,
String? initialRowId,
) { ) {
return CalendarPage( return CalendarPage(
key: _makeValueKey(controller), key: _makeValueKey(controller),

View File

@ -1,9 +1,15 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
@ -11,22 +17,22 @@ import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import '../../application/database_controller.dart';
import '../../application/row/row_cache.dart'; import '../../application/row/row_cache.dart';
import '../../application/row/row_controller.dart'; import '../../application/row/row_controller.dart';
import '../application/grid_bloc.dart';
import '../../application/database_controller.dart';
import 'grid_scroll.dart';
import '../../tab_bar/tab_bar_view.dart'; import '../../tab_bar/tab_bar_view.dart';
import '../../widgets/row/row_detail.dart';
import '../application/grid_bloc.dart';
import 'grid_scroll.dart';
import 'layout/layout.dart'; import 'layout/layout.dart';
import 'layout/sizes.dart'; import 'layout/sizes.dart';
import 'widgets/row/row.dart';
import 'widgets/footer/grid_footer.dart'; import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart'; import 'widgets/header/grid_header.dart';
import '../../widgets/row/row_detail.dart'; import 'widgets/row/row.dart';
import 'widgets/shortcuts.dart'; import 'widgets/shortcuts.dart';
class ToggleExtensionNotifier extends ChangeNotifier { class ToggleExtensionNotifier extends ChangeNotifier {
@ -49,11 +55,13 @@ class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
ViewPB view, ViewPB view,
DatabaseController controller, DatabaseController controller,
bool shrinkWrap, bool shrinkWrap,
String? initialRowId,
) { ) {
return GridPage( return GridPage(
key: _makeValueKey(controller), key: _makeValueKey(controller),
view: view, view: view,
databaseController: controller, databaseController: controller,
initialRowId: initialRowId,
); );
} }
@ -85,31 +93,33 @@ class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
} }
class GridPage extends StatefulWidget { class GridPage extends StatefulWidget {
final DatabaseController databaseController;
const GridPage({ const GridPage({
super.key,
required this.view, required this.view,
required this.databaseController, required this.databaseController,
this.onDeleted, this.onDeleted,
super.key, this.initialRowId,
}); });
final ViewPB view; final ViewPB view;
final DatabaseController databaseController;
final VoidCallback? onDeleted; final VoidCallback? onDeleted;
final String? initialRowId;
@override @override
State<GridPage> createState() => _GridPageState(); State<GridPage> createState() => _GridPageState();
} }
class _GridPageState extends State<GridPage> { class _GridPageState extends State<GridPage> {
@override bool _didOpenInitialRow = false;
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider<NotificationActionBloc>.value(
value: getIt<NotificationActionBloc>(),
),
BlocProvider<GridBloc>( BlocProvider<GridBloc>(
create: (context) => GridBloc( create: (context) => GridBloc(
view: widget.view, view: widget.view,
@ -117,35 +127,88 @@ class _GridPageState extends State<GridPage> {
)..add(const GridEvent.initial()), )..add(const GridEvent.initial()),
), ),
], ],
child: BlocBuilder<GridBloc, GridState>( child: BlocListener<NotificationActionBloc, NotificationActionState>(
builder: (context, state) { listener: (context, state) {
return state.loadingState.map( final action = state.action;
if (action?.type == ActionType.openRow &&
action?.objectId == widget.view.id) {
final rowId = action!.arguments?[ActionArgumentKeys.rowId];
if (rowId != null) {
// If Reminder in existing database is pressed
// then open the row
_openRow(context, rowId);
}
}
},
child: BlocConsumer<GridBloc, GridState>(
listener: (context, state) => state.loadingState.whenOrNull(
// If initial row id is defined, open row details overlay
finish: (_) {
if (widget.initialRowId != null && !_didOpenInitialRow) {
_didOpenInitialRow = true;
_openRow(context, widget.initialRowId!);
}
return;
},
),
builder: (context, state) => state.loadingState.map(
loading: (_) => loading: (_) =>
const Center(child: CircularProgressIndicator.adaptive()), const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) => result.successOrFail.fold( finish: (result) => result.successOrFail.fold(
(_) => GridShortcuts( (_) => GridShortcuts(child: GridPageContent(view: widget.view)),
child: GridPageContent(view: widget.view),
),
(err) => FlowyErrorPage.message( (err) => FlowyErrorPage.message(
err.toString(), err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
), ),
), ),
idle: (_) => const SizedBox.shrink(), idle: (_) => const SizedBox.shrink(),
); ),
}, ),
), ),
); );
} }
void _openRow(
BuildContext context,
String rowId,
) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final gridBloc = context.read<GridBloc>();
final rowCache = gridBloc.getRowCache(rowId);
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
if (rowMeta == null) {
return;
}
final fieldController = gridBloc.databaseController.fieldController;
final rowController = RowController(
viewId: widget.view.id,
rowMeta: rowMeta,
rowCache: rowCache,
);
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
cellBuilder: GridCellBuilder(cellCache: rowController.cellCache),
rowController: rowController,
fieldController: fieldController,
),
);
});
}
} }
class GridPageContent extends StatefulWidget { class GridPageContent extends StatefulWidget {
final ViewPB view;
const GridPageContent({ const GridPageContent({
required this.view,
super.key, super.key,
required this.view,
}); });
final ViewPB view;
@override @override
State<GridPageContent> createState() => _GridPageContentState(); State<GridPageContent> createState() => _GridPageContentState();
} }

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
@ -7,6 +9,8 @@ import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/shortcuts.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/shortcuts.dart';
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@ -15,7 +19,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart';
@ -33,11 +36,13 @@ class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
ViewPB view, ViewPB view,
DatabaseController controller, DatabaseController controller,
bool shrinkWrap, bool shrinkWrap,
String? initialRowId,
) { ) {
return MobileGridPage( return MobileGridPage(
key: _makeValueKey(controller), key: _makeValueKey(controller),
view: view, view: view,
databaseController: controller, databaseController: controller,
initialRowId: initialRowId,
); );
} }
@ -58,26 +63,33 @@ class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
} }
class MobileGridPage extends StatefulWidget { class MobileGridPage extends StatefulWidget {
final DatabaseController databaseController;
const MobileGridPage({ const MobileGridPage({
super.key,
required this.view, required this.view,
required this.databaseController, required this.databaseController,
this.onDeleted, this.onDeleted,
super.key, this.initialRowId,
}); });
final ViewPB view; final ViewPB view;
final DatabaseController databaseController;
final VoidCallback? onDeleted; final VoidCallback? onDeleted;
final String? initialRowId;
@override @override
State<MobileGridPage> createState() => _MobileGridPageState(); State<MobileGridPage> createState() => _MobileGridPageState();
} }
class _MobileGridPageState extends State<MobileGridPage> { class _MobileGridPageState extends State<MobileGridPage> {
bool _didOpenInitialRow = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider<NotificationActionBloc>.value(
value: getIt<NotificationActionBloc>(),
),
BlocProvider<GridBloc>( BlocProvider<GridBloc>(
create: (context) => GridBloc( create: (context) => GridBloc(
view: widget.view, view: widget.view,
@ -90,19 +102,43 @@ class _MobileGridPageState extends State<MobileGridPage> {
return state.loadingState.map( return state.loadingState.map(
loading: (_) => loading: (_) =>
const Center(child: CircularProgressIndicator.adaptive()), const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) => result.successOrFail.fold( finish: (result) {
_openRow(context, widget.initialRowId, true);
return result.successOrFail.fold(
(_) => GridShortcuts(child: GridPageContent(view: widget.view)), (_) => GridShortcuts(child: GridPageContent(view: widget.view)),
(err) => FlowyErrorPage.message( (err) => FlowyErrorPage.message(
err.toString(), err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
), ),
), );
},
idle: (_) => const SizedBox.shrink(), idle: (_) => const SizedBox.shrink(),
); );
}, },
), ),
); );
} }
void _openRow(
BuildContext context,
String? rowId, [
bool initialRow = false,
]) {
if (rowId != null && (!initialRow || (initialRow && !_didOpenInitialRow))) {
_didOpenInitialRow = initialRow;
WidgetsBinding.instance.addPostFrameCallback((_) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowId,
MobileRowDetailPage.argDatabaseController:
widget.databaseController,
},
);
});
}
}
} }
class GridPageContent extends StatefulWidget { class GridPageContent extends StatefulWidget {

View File

@ -1,3 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import "package:appflowy/generated/locale_keys.g.dart"; import "package:appflowy/generated/locale_keys.g.dart";
import 'package:appflowy/plugins/database/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
@ -9,14 +12,13 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../../widgets/row/accessory/cell_accessory.dart'; import '../../../../widgets/row/accessory/cell_accessory.dart';
import '../../../../widgets/row/cells/cell_container.dart'; import '../../../../widgets/row/cells/cell_container.dart';
import '../../layout/sizes.dart'; import '../../layout/sizes.dart';
import 'action.dart'; import 'action.dart';
class GridRow extends StatefulWidget { class GridRow extends StatefulWidget {
@ -186,15 +188,15 @@ class InsertRowButton extends StatelessWidget {
} }
class RowMenuButton extends StatefulWidget { class RowMenuButton extends StatefulWidget {
final VoidCallback openMenu;
final bool isDragEnabled;
const RowMenuButton({ const RowMenuButton({
super.key,
required this.openMenu, required this.openMenu,
this.isDragEnabled = false, this.isDragEnabled = false,
super.key,
}); });
final VoidCallback openMenu;
final bool isDragEnabled;
@override @override
State<RowMenuButton> createState() => _RowMenuButtonState(); State<RowMenuButton> createState() => _RowMenuButtonState();
} }
@ -227,14 +229,15 @@ class _RowMenuButtonState extends State<RowMenuButton> {
} }
class RowContent extends StatelessWidget { class RowContent extends StatelessWidget {
final VoidCallback onExpand;
final GridCellBuilder builder;
const RowContent({ const RowContent({
super.key,
required this.builder, required this.builder,
required this.onExpand, required this.onExpand,
super.key,
}); });
final GridCellBuilder builder;
final VoidCallback onExpand;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<RowBloc, RowState>( return BlocBuilder<RowBloc, RowState>(

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
@ -10,7 +12,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'tab_bar_add_button.dart'; import 'tab_bar_add_button.dart';
@ -37,9 +38,7 @@ class TabBarHeader extends StatelessWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Flexible( const Flexible(child: DatabaseTabBar()),
child: DatabaseTabBar(),
),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) { builder: (context, state) {
return SizedBox( return SizedBox(

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
@ -10,7 +12,6 @@ import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'desktop/tab_bar_header.dart'; import 'desktop/tab_bar_header.dart';
@ -26,6 +27,7 @@ abstract class DatabaseTabBarItemBuilder {
ViewPB view, ViewPB view,
DatabaseController controller, DatabaseController controller,
bool shrinkWrap, bool shrinkWrap,
String? initialRowId,
); );
/// Returns the setting bar of the tab bar item. The setting bar is shown on the /// Returns the setting bar of the tab bar item. The setting bar is shown on the
@ -44,10 +46,16 @@ abstract class DatabaseTabBarItemBuilder {
class DatabaseTabBarView extends StatefulWidget { class DatabaseTabBarView extends StatefulWidget {
final ViewPB view; final ViewPB view;
final bool shrinkWrap; final bool shrinkWrap;
/// Used to open a Row on plugin load
///
final String? initialRowId;
const DatabaseTabBarView({ const DatabaseTabBarView({
super.key,
required this.view, required this.view,
required this.shrinkWrap, required this.shrinkWrap,
super.key, this.initialRowId,
}); });
@override @override
@ -55,19 +63,12 @@ class DatabaseTabBarView extends StatefulWidget {
} }
class _DatabaseTabBarViewState extends State<DatabaseTabBarView> { class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
PageController? _pageController; final PageController _pageController = PageController(initialPage: 0);
late String? _initialRowId = widget.initialRowId;
@override
void initState() {
super.initState();
_pageController = PageController(
initialPage: 0,
);
}
@override @override
void dispose() { void dispose() {
_pageController?.dispose(); _pageController.dispose();
super.dispose(); super.dispose();
} }
@ -75,15 +76,14 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<DatabaseTabBarBloc>( return BlocProvider<DatabaseTabBarBloc>(
create: (context) => DatabaseTabBarBloc(view: widget.view) create: (context) => DatabaseTabBarBloc(view: widget.view)
..add( ..add(const DatabaseTabBarEvent.initial()),
const DatabaseTabBarEvent.initial(),
),
child: MultiBlocListener( child: MultiBlocListener(
listeners: [ listeners: [
BlocListener<DatabaseTabBarBloc, DatabaseTabBarState>( BlocListener<DatabaseTabBarBloc, DatabaseTabBarState>(
listenWhen: (p, c) => p.selectedIndex != c.selectedIndex, listenWhen: (p, c) => p.selectedIndex != c.selectedIndex,
listener: (context, state) { listener: (context, state) {
_pageController?.jumpToPage(state.selectedIndex); _initialRowId = null;
_pageController.jumpToPage(state.selectedIndex);
}, },
), ),
], ],
@ -120,20 +120,17 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
}, },
), ),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) { builder: (context, state) =>
return pageSettingBarExtensionFromState(state); pageSettingBarExtensionFromState(state),
},
), ),
Expanded( Expanded(
child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) { builder: (context, state) => PageView(
return PageView(
pageSnapping: false, pageSnapping: false,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
controller: _pageController, controller: _pageController,
children: pageContentFromState(state), children: pageContentFromState(state),
); ),
},
), ),
), ),
], ],
@ -146,11 +143,13 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
return state.tabBars.map((tabBar) { return state.tabBars.map((tabBar) {
final controller = final controller =
state.tabBarControllerByViewId[tabBar.viewId]!.controller; state.tabBarControllerByViewId[tabBar.viewId]!.controller;
return tabBar.builder.content( return tabBar.builder.content(
context, context,
tabBar.view, tabBar.view,
controller, controller,
widget.shrinkWrap, widget.shrinkWrap,
_initialRowId,
); );
}).toList(); }).toList();
} }
@ -174,15 +173,21 @@ class DatabaseTabBarViewPlugin extends Plugin {
final ViewPluginNotifier notifier; final ViewPluginNotifier notifier;
final PluginType _pluginType; final PluginType _pluginType;
/// Used to open a Row on plugin load
///
final String? initialRowId;
DatabaseTabBarViewPlugin({ DatabaseTabBarViewPlugin({
required ViewPB view, required ViewPB view,
required PluginType pluginType, required PluginType pluginType,
this.initialRowId,
}) : _pluginType = pluginType, }) : _pluginType = pluginType,
notifier = ViewPluginNotifier(view: view); notifier = ViewPluginNotifier(view: view);
@override @override
PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder( PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder(
notifier: notifier, notifier: notifier,
initialRowId: initialRowId,
); );
@override @override
@ -195,9 +200,14 @@ class DatabaseTabBarViewPlugin extends Plugin {
class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier; final ViewPluginNotifier notifier;
/// Used to open a Row on plugin load
///
final String? initialRowId;
DatabasePluginWidgetBuilder({ DatabasePluginWidgetBuilder({
required this.notifier,
Key? key, Key? key,
required this.notifier,
this.initialRowId,
}); });
@override @override
@ -219,6 +229,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
key: ValueKey(notifier.view.id), key: ValueKey(notifier.view.id),
view: notifier.view, view: notifier.view,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
initialRowId: initialRowId,
); );
} }

View File

@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.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/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:styled_widget/styled_widget.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'; import '../cell_builder.dart';
@ -52,14 +52,15 @@ abstract mixin class GridCellAccessoryState {
} }
class PrimaryCellAccessory extends StatefulWidget { class PrimaryCellAccessory extends StatefulWidget {
final VoidCallback onTapCallback;
final bool isCellEditing;
const PrimaryCellAccessory({ const PrimaryCellAccessory({
super.key,
required this.onTapCallback, required this.onTapCallback,
required this.isCellEditing, required this.isCellEditing,
super.key,
}); });
final VoidCallback onTapCallback;
final bool isCellEditing;
@override @override
State<StatefulWidget> createState() => _PrimaryCellAccessoryState(); 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/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.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/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart'; import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart'; import '../../cell_builder.dart';
import 'date_cell_bloc.dart'; import 'date_cell_bloc.dart';
import 'date_editor.dart'; import 'date_editor.dart';
@ -85,22 +91,32 @@ class _DateCellState extends GridCellState<GridDateCell> {
child: Container( child: Container(
alignment: alignment, alignment: alignment,
padding: padding, padding: padding,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium( child: FlowyText.medium(
text, text,
color: color, color: color,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
popupBuilder: (BuildContext popoverContent) { if (state.data?.reminderId.isNotEmpty == true) ...[
return DateCellEditor( const HSpace(5),
FlowyTooltip(
message:
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
],
],
),
),
popupBuilder: (_) => DateCellEditor(
cellController: _cellController, cellController: _cellController,
onDismissed: () => onDismissed: () => widget.cellContainerNotifier.isFocus = false,
widget.cellContainerNotifier.isFocus = false, ),
); onClose: () => widget.cellContainerNotifier.isFocus = false,
},
onClose: () {
widget.cellContainerNotifier.isFocus = false;
},
); );
} else if (widget.cellStyle.useRoundedBorder) { } else if (widget.cellStyle.useRoundedBorder) {
return InkWell( return InkWell(
@ -108,12 +124,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
onTap: () => showMobileBottomSheet( onTap: () => showMobileBottomSheet(
context, context,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
builder: (context) { builder: (_) => MobileDateCellEditScreen(
return MobileDateCellEditScreen(
controller: _cellController, controller: _cellController,
showAsFullScreen: false, showAsFullScreen: false,
); ),
},
), ),
child: Container( child: Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
@ -146,28 +160,36 @@ class _DateCellState extends GridCellState<GridDateCell> {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10), const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: FlowyText( 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, text,
color: color, color: color,
fontSize: 15, fontSize: 15,
maxLines: 1, maxLines: 1,
), ),
],
), ),
), ),
onTap: () { ),
showMobileBottomSheet( onTap: () => showMobileBottomSheet(
context, context,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.secondaryContainer, Theme.of(context).colorScheme.secondaryContainer,
builder: (context) { builder: (_) => MobileDateCellEditScreen(
return MobileDateCellEditScreen(
controller: _cellController, controller: _cellController,
showAsFullScreen: false, 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/cell/date_cell_service.dart';
import 'package:appflowy/plugins/database/application/field/field_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/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/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart' import 'package:easy_localization/easy_localization.dart'
show StringTranslateExtension; show StringTranslateExtension;
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra/time/duration.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:nanoid/non_secure.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
part 'date_cell_editor_bloc.freezed.dart'; part 'date_cell_editor_bloc.freezed.dart';
@ -22,16 +29,19 @@ class DateCellEditorBloc
extends Bloc<DateCellEditorEvent, DateCellEditorState> { extends Bloc<DateCellEditorEvent, DateCellEditorState> {
final DateCellBackendService _dateCellBackendService; final DateCellBackendService _dateCellBackendService;
final DateCellController cellController; final DateCellController cellController;
final ReminderBloc _reminderBloc;
void Function()? _onCellChangedFn; void Function()? _onCellChangedFn;
DateCellEditorBloc({ DateCellEditorBloc({
required this.cellController, required this.cellController,
}) : _dateCellBackendService = DateCellBackendService( required ReminderBloc reminderBloc,
}) : _reminderBloc = reminderBloc,
_dateCellBackendService = DateCellBackendService(
viewId: cellController.viewId, viewId: cellController.viewId,
fieldId: cellController.fieldId, fieldId: cellController.fieldId,
rowId: cellController.rowId, rowId: cellController.rowId,
), ),
super(DateCellEditorState.initial(cellController)) { super(DateCellEditorState.initial(cellController, reminderBloc)) {
on<DateCellEditorEvent>( on<DateCellEditorEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
@ -42,6 +52,41 @@ class DateCellEditorBloc
dateCellData.isRange == state.isRange && dateCellData.isRange dateCellData.isRange == state.isRange && dateCellData.isRange
? dateCellData.endDateTime ? dateCellData.endDateTime
: null; : 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( emit(
state.copyWith( state.copyWith(
dateTime: dateCellData.dateTime, dateTime: dateCellData.dateTime,
@ -54,11 +99,14 @@ class DateCellEditorBloc
endDay: endDay, endDay: endDay,
dateStr: dateCellData.dateStr, dateStr: dateCellData.dateStr,
endDateStr: dateCellData.endDateStr, endDateStr: dateCellData.endDateStr,
reminderId: dateCellData.reminderId,
), ),
); );
}, },
didReceiveTimeFormatError: didReceiveTimeFormatError: (
(String? parseTimeError, String? parseEndTimeError) { String? parseTimeError,
String? parseEndTimeError,
) {
emit( emit(
state.copyWith( state.copyWith(
parseTimeError: parseTimeError, parseTimeError: parseTimeError,
@ -67,17 +115,14 @@ class DateCellEditorBloc
); );
}, },
selectDay: (date) async { selectDay: (date) async {
if (state.isRange) { if (!state.isRange) {
return;
}
await _updateDateData(date: date); await _updateDateData(date: date);
}
}, },
setIncludeTime: (includeTime) async { setIncludeTime: (includeTime) async =>
await _updateDateData(includeTime: includeTime); await _updateDateData(includeTime: includeTime),
}, setIsRange: (isRange) async =>
setIsRange: (isRange) async { await _updateDateData(isRange: isRange),
await _updateDateData(isRange: isRange);
},
setTime: (timeStr) async { setTime: (timeStr) async {
emit(state.copyWith(timeStr: timeStr)); emit(state.copyWith(timeStr: timeStr));
await _updateDateData(timeStr: timeStr); await _updateDateData(timeStr: timeStr);
@ -87,89 +132,88 @@ class DateCellEditorBloc
final (newStart, newEnd) = state.startDay!.isBefore(start!) final (newStart, newEnd) = state.startDay!.isBefore(start!)
? (state.startDay!, start) ? (state.startDay!, start)
: (start, state.startDay!); : (start, state.startDay!);
emit(
state.copyWith( emit(state.copyWith(startDay: null, endDay: null));
startDay: null,
endDay: null, await _updateDateData(date: newStart.date, endDate: newEnd.date);
),
);
await _updateDateData(
date: newStart.date,
endDate: newEnd.date,
);
} else if (end == null) { } else if (end == null) {
emit( emit(state.copyWith(startDay: start, endDay: null));
state.copyWith(
startDay: start,
endDay: null,
),
);
} else { } else {
await _updateDateData( await _updateDateData(date: start!.date, endDate: end.date);
date: start!.date,
endDate: end.date,
);
} }
}, },
setStartDay: (DateTime startDay) async { setStartDay: (DateTime startDay) async {
if (state.endDay == null) { if (state.endDay == null) {
emit( emit(state.copyWith(startDay: startDay));
state.copyWith(
startDay: startDay,
),
);
} else if (startDay.isAfter(state.endDay!)) { } else if (startDay.isAfter(state.endDay!)) {
emit( emit(state.copyWith(startDay: startDay, endDay: null));
state.copyWith(
startDay: startDay,
endDay: null,
),
);
} else { } else {
emit( emit(state.copyWith(startDay: startDay));
state.copyWith( await _updateDateData(
startDay: startDay, 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) { if (state.startDay == null) {
emit( emit(state.copyWith(endDay: endDay));
state.copyWith(
endDay: endDay,
),
);
} else if (endDay.isBefore(state.startDay!)) { } else if (endDay.isBefore(state.startDay!)) {
emit( emit(state.copyWith(startDay: null, endDay: endDay));
state.copyWith(
startDay: null,
endDay: endDay,
),
);
} else { } else {
emit( emit(state.copyWith(endDay: endDay));
state.copyWith(
endDay: endDay,
),
);
_updateDateData(date: state.startDay!.date, endDate: endDay.date); _updateDateData(date: state.startDay!.date, endDate: endDay.date);
} }
}, },
setEndTime: (String endTime) async { setEndTime: (String? endTime) async {
emit(state.copyWith(endTimeStr: endTime)); emit(state.copyWith(endTimeStr: endTime));
await _updateDateData(endTimeStr: endTime); await _updateDateData(endTimeStr: endTime);
}, },
setDateFormat: (dateFormat) async { setDateFormat: (DateFormatPB dateFormat) async =>
await _updateTypeOption(emit, dateFormat: dateFormat); await _updateTypeOption(emit, dateFormat: dateFormat),
}, setTimeFormat: (TimeFormatPB timeFormat) async =>
setTimeFormat: (timeFormat) async { await _updateTypeOption(emit, timeFormat: timeFormat),
await _updateTypeOption(emit, timeFormat: timeFormat);
},
clearDate: () async { clearDate: () async {
// Remove reminder if neccessary
if (state.reminderId != null) {
_reminderBloc
.add(ReminderEvent.remove(reminderId: state.reminderId!));
}
await _clearDate(); 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, String? endTimeStr,
bool? includeTime, bool? includeTime,
bool? isRange, bool? isRange,
String? reminderId,
}) async { }) async {
// make sure that not both date and time are updated at the same time // make sure that not both date and time are updated at the same time
assert( assert(
@ -191,21 +236,15 @@ class DateCellEditorBloc
// if not updating the time, use the old time in the state // if not updating the time, use the old time in the state
final String? newTime = timeStr ?? state.timeStr; final String? newTime = timeStr ?? state.timeStr;
DateTime? newDate; final DateTime? newDate = timeStr != null && timeStr.isNotEmpty
if (timeStr != null && timeStr.isNotEmpty) { ? state.dateTime ?? DateTime.now()
newDate = state.dateTime ?? DateTime.now(); : _utcToLocalAndAddCurrentTime(date);
} else {
newDate = _utcToLocalAndAddCurrentTime(date);
}
// if not updating the time, use the old time in the state // if not updating the time, use the old time in the state
final String? newEndTime = endTimeStr ?? state.endTimeStr; final String? newEndTime = endTimeStr ?? state.endTimeStr;
DateTime? newEndDate; final DateTime? newEndDate = endTimeStr != null && endTimeStr.isNotEmpty
if (endTimeStr != null && endTimeStr.isNotEmpty) { ? state.endDateTime ?? DateTime.now()
newEndDate = state.endDateTime ?? DateTime.now(); : _utcToLocalAndAddCurrentTime(endDate);
} else {
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
}
final result = await _dateCellBackendService.update( final result = await _dateCellBackendService.update(
date: newDate, date: newDate,
@ -214,15 +253,14 @@ class DateCellEditorBloc
endTime: newEndTime, endTime: newEndTime,
includeTime: includeTime ?? state.includeTime, includeTime: includeTime ?? state.includeTime,
isRange: isRange ?? state.isRange, isRange: isRange ?? state.isRange,
reminderId: reminderId ?? state.reminderId,
); );
result.fold( result.fold(
(_) { (_) {
if (!isClosed && if (!isClosed &&
(state.parseEndTimeError != null || state.parseTimeError != null)) { (state.parseEndTimeError != null || state.parseTimeError != null)) {
add( add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null));
const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
);
} }
}, },
(err) { (err) {
@ -231,10 +269,12 @@ class DateCellEditorBloc
if (isClosed) { if (isClosed) {
return; return;
} }
// to determine which textfield should show error // to determine which textfield should show error
final (startError, endError) = newDate != null final (startError, endError) = newDate != null
? (timeFormatPrompt(err), null) ? (timeFormatPrompt(err), null)
: (null, timeFormatPrompt(err)); : (null, timeFormatPrompt(err));
add( add(
DateCellEditorEvent.didReceiveTimeFormatError( DateCellEditorEvent.didReceiveTimeFormatError(
startError, startError,
@ -253,13 +293,9 @@ class DateCellEditorBloc
final result = await _dateCellBackendService.clear(); final result = await _dateCellBackendService.clear();
result.fold( result.fold(
(_) { (_) {
if (isClosed) { if (!isClosed) {
return; add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null));
} }
add(
const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
);
}, },
(err) => Log.error(err), (err) => Log.error(err),
); );
@ -304,11 +340,11 @@ class DateCellEditorBloc
void _startListening() { void _startListening() {
_onCellChangedFn = cellController.startListening( _onCellChangedFn = cellController.startListening(
onCellChanged: ((cell) { onCellChanged: (cell) {
if (!isClosed) { if (!isClosed) {
add(DateCellEditorEvent.didReceiveCellUpdate(cell)); add(DateCellEditorEvent.didReceiveCellUpdate(cell));
} }
}), },
); );
} }
@ -335,7 +371,7 @@ class DateCellEditorBloc
); );
result.fold( result.fold(
(l) => emit( (_) => emit(
state.copyWith( state.copyWith(
dateTypeOptionPB: newDateTypeOption, dateTypeOptionPB: newDateTypeOption,
timeHintText: _timeHintText(newDateTypeOption), timeHintText: _timeHintText(newDateTypeOption),
@ -355,6 +391,7 @@ class DateCellEditorEvent with _$DateCellEditorEvent {
const factory DateCellEditorEvent.didReceiveCellUpdate( const factory DateCellEditorEvent.didReceiveCellUpdate(
DateCellDataPB? data, DateCellDataPB? data,
) = _DidReceiveCellUpdate; ) = _DidReceiveCellUpdate;
const factory DateCellEditorEvent.didReceiveTimeFormatError( const factory DateCellEditorEvent.didReceiveTimeFormatError(
String? parseTimeError, String? parseTimeError,
String? parseEndTimeError, String? parseEndTimeError,
@ -362,27 +399,41 @@ class DateCellEditorEvent with _$DateCellEditorEvent {
// date cell data is modified // date cell data is modified
const factory DateCellEditorEvent.selectDay(DateTime day) = _SelectDay; const factory DateCellEditorEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellEditorEvent.selectDateRange( const factory DateCellEditorEvent.selectDateRange(
DateTime? start, DateTime? start,
DateTime? end, DateTime? end,
) = _SelectDateRange; ) = _SelectDateRange;
const factory DateCellEditorEvent.setStartDay( const factory DateCellEditorEvent.setStartDay(
DateTime startDay, DateTime startDay,
) = _SetStartDay; ) = _SetStartDay;
const factory DateCellEditorEvent.setEndDay( const factory DateCellEditorEvent.setEndDay(
DateTime endDay, DateTime endDay,
) = _SetEndDay; ) = _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) = const factory DateCellEditorEvent.setIncludeTime(bool includeTime) =
_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 // date field type options are modified
const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) = const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) =
_TimeFormat; _SetTimeFormat;
const factory DateCellEditorEvent.setDateFormat(DateFormatPB dateFormat) = const factory DateCellEditorEvent.setDateFormat(DateFormatPB dateFormat) =
_DateFormat; _SetDateFormat;
const factory DateCellEditorEvent.clearDate() = _ClearDate; const factory DateCellEditorEvent.clearDate() = _ClearDate;
} }
@ -406,17 +457,36 @@ class DateCellEditorState with _$DateCellEditorState {
required bool isRange, required bool isRange,
required String? dateStr, required String? dateStr,
required String? endDateStr, required String? endDateStr,
required String? reminderId,
// error and hint text // error and hint text
required String? parseTimeError, required String? parseTimeError,
required String? parseEndTimeError, required String? parseEndTimeError,
required String timeHintText, required String timeHintText,
@Default(ReminderOption.none) ReminderOption reminderOption,
}) = _DateCellEditorState; }) = _DateCellEditorState;
factory DateCellEditorState.initial(DateCellController controller) { factory DateCellEditorState.initial(
DateCellController controller,
ReminderBloc reminderBloc,
) {
final typeOption = controller.getTypeOption(DateTypeOptionDataParser()); final typeOption = controller.getTypeOption(DateTypeOptionDataParser());
final cellData = controller.getCellData(); final cellData = controller.getCellData();
final dateCellData = _dateDataFromCellData(cellData); 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( return DateCellEditorState(
dateTypeOptionPB: typeOption, dateTypeOptionPB: typeOption,
startDay: dateCellData.isRange ? dateCellData.dateTime : null, startDay: dateCellData.isRange ? dateCellData.dateTime : null,
@ -432,6 +502,8 @@ class DateCellEditorState with _$DateCellEditorState {
parseTimeError: null, parseTimeError: null,
parseEndTimeError: null, parseEndTimeError: null,
timeHintText: _timeHintText(typeOption), timeHintText: _timeHintText(typeOption),
reminderId: dateCellData.reminderId,
reminderOption: reminderOption,
); );
} }
} }
@ -462,6 +534,7 @@ _DateCellData _dateDataFromCellData(
isRange: false, isRange: false,
dateStr: null, dateStr: null,
endDateStr: null, endDateStr: null,
reminderId: null,
); );
} }
@ -481,12 +554,14 @@ _DateCellData _dateDataFromCellData(
endTimeStr = cellData.endTime; endTimeStr = cellData.endTime;
} }
} }
final bool includeTime = cellData.includeTime; final bool includeTime = cellData.includeTime;
final bool isRange = cellData.isRange; final bool isRange = cellData.isRange;
if (cellData.isRange) { if (cellData.isRange) {
endDateStr = cellData.endDate; endDateStr = cellData.endDate;
} }
final String dateStr = cellData.date; final String dateStr = cellData.date;
return _DateCellData( return _DateCellData(
@ -498,6 +573,7 @@ _DateCellData _dateDataFromCellData(
isRange: isRange, isRange: isRange,
dateStr: dateStr, dateStr: dateStr,
endDateStr: endDateStr, endDateStr: endDateStr,
reminderId: cellData.reminderId,
); );
} }
@ -510,6 +586,7 @@ class _DateCellData {
final bool isRange; final bool isRange;
final String? dateStr; final String? dateStr;
final String? endDateStr; final String? endDateStr;
final String? reminderId;
_DateCellData({ _DateCellData({
required this.dateTime, required this.dateTime,
@ -520,5 +597,6 @@ class _DateCellData {
required this.isRange, required this.isRange,
required this.dateStr, required this.dateStr,
required this.endDateStr, 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: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 'package:flutter_bloc/flutter_bloc.dart';
import 'date_cell_editor_bloc.dart'; import 'date_cell_editor_bloc.dart';
@ -31,20 +36,28 @@ class _DateCellEditor extends State<DateCellEditor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return MultiBlocProvider(
providers: [
BlocProvider<DateCellEditorBloc>(
create: (context) => DateCellEditorBloc( create: (context) => DateCellEditorBloc(
reminderBloc: getIt<ReminderBloc>(),
cellController: widget.cellController, cellController: widget.cellController,
)..add(const DateCellEditorEvent.initial()), )..add(const DateCellEditorEvent.initial()),
),
],
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>( child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) { builder: (context, state) {
final bloc = context.read<DateCellEditorBloc>(); final dateCellBloc = context.read<DateCellEditorBloc>();
return AppFlowyDatePicker( return AppFlowyDatePicker(
includeTime: state.includeTime, includeTime: state.includeTime,
rebuildOnDaySelected: false,
onIncludeTimeChanged: (value) => onIncludeTimeChanged: (value) =>
bloc.add(DateCellEditorEvent.setIncludeTime(!value)), dateCellBloc.add(DateCellEditorEvent.setIncludeTime(!value)),
isRange: state.isRange, isRange: state.isRange,
startDay: state.isRange ? state.startDay : null,
endDay: state.isRange ? state.endDay : null,
onIsRangeChanged: (value) => onIsRangeChanged: (value) =>
bloc.add(DateCellEditorEvent.setIsRange(!value)), dateCellBloc.add(DateCellEditorEvent.setIsRange(!value)),
dateFormat: state.dateTypeOptionPB.dateFormat, dateFormat: state.dateTypeOptionPB.dateFormat,
timeFormat: state.dateTypeOptionPB.timeFormat, timeFormat: state.dateTypeOptionPB.timeFormat,
selectedDay: state.dateTime, selectedDay: state.dateTime,
@ -54,28 +67,36 @@ class _DateCellEditor extends State<DateCellEditor> {
parseEndTimeError: state.parseEndTimeError, parseEndTimeError: state.parseEndTimeError,
parseTimeError: state.parseTimeError, parseTimeError: state.parseTimeError,
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
onStartTimeSubmitted: (timeStr) { onReminderSelected: (option) => dateCellBloc
bloc.add(DateCellEditorEvent.setTime(timeStr)); .add(DateCellEditorEvent.setReminderOption(option: option)),
}, selectedReminderOption: state.reminderOption,
onEndTimeSubmitted: (timeStr) { options: [
bloc.add(DateCellEditorEvent.setEndTime(timeStr)); OptionGroup(
}, options: [
onDaySelected: (selectedDay, _) { DateTypeOptionButton(
bloc.add(DateCellEditorEvent.selectDay(selectedDay)); popoverMutex: popoverMutex,
}, dateFormat: state.dateTypeOptionPB.dateFormat,
onRangeSelected: (start, end, _) { timeFormat: state.dateTypeOptionPB.timeFormat,
bloc.add(DateCellEditorEvent.selectDateRange(start, end)); onDateFormatChanged: (format) => dateCellBloc
}, .add(DateCellEditorEvent.setDateFormat(format)),
allowFormatChanges: true, onTimeFormatChanged: (format) => dateCellBloc
onDateFormatChanged: (format) { .add(DateCellEditorEvent.setTimeFormat(format)),
bloc.add(DateCellEditorEvent.setDateFormat(format)); ),
}, ClearDateButton(
onTimeFormatChanged: (format) { onClearDate: () =>
bloc.add(DateCellEditorEvent.setTimeFormat(format)); dateCellBloc.add(const DateCellEditorEvent.clearDate()),
}, ),
onClearDate: () { ],
bloc.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),
],
);
}
}

View File

@ -1,5 +1,7 @@
library document_plugin; library document_plugin;
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/plugins/document/document_page.dart';
@ -12,9 +14,9 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class DocumentPluginBuilder extends PluginBuilder { class DocumentPluginBuilder extends PluginBuilder {
@ -22,9 +24,9 @@ class DocumentPluginBuilder extends PluginBuilder {
Plugin build(dynamic data) { Plugin build(dynamic data) {
if (data is ViewPB) { if (data is ViewPB) {
return DocumentPlugin(pluginType: pluginType, view: data); return DocumentPlugin(pluginType: pluginType, view: data);
} else {
throw FlowyPluginException.invalidData;
} }
throw FlowyPluginException.invalidData;
} }
@override @override
@ -41,26 +43,28 @@ class DocumentPluginBuilder extends PluginBuilder {
} }
class DocumentPlugin extends Plugin<int> { class DocumentPlugin extends Plugin<int> {
DocumentPlugin({
Key? key,
required ViewPB view,
required PluginType pluginType,
bool listenOnViewChanged = false,
this.initialSelection,
}) : notifier = ViewPluginNotifier(view: view) {
_pluginType = pluginType;
}
late PluginType _pluginType; late PluginType _pluginType;
@override @override
final ViewPluginNotifier notifier; final ViewPluginNotifier notifier;
DocumentPlugin({ final Selection? initialSelection;
required PluginType pluginType,
required ViewPB view,
bool listenOnViewChanged = false,
Key? key,
}) : notifier = ViewPluginNotifier(view: view) {
_pluginType = pluginType;
}
@override @override
PluginWidgetBuilder get widgetBuilder { PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder(
return DocumentPluginWidgetBuilder(
notifier: notifier, notifier: notifier,
initialSelection: initialSelection,
); );
}
@override @override
PluginType get pluginType => _pluginType; PluginType get pluginType => _pluginType;
@ -71,14 +75,16 @@ class DocumentPlugin extends Plugin<int> {
class DocumentPluginWidgetBuilder extends PluginWidgetBuilder class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
with NavigationItem { with NavigationItem {
DocumentPluginWidgetBuilder({
Key? key,
required this.notifier,
this.initialSelection,
});
final ViewPluginNotifier notifier; final ViewPluginNotifier notifier;
ViewPB get view => notifier.view; ViewPB get view => notifier.view;
int? deletedViewIndex; int? deletedViewIndex;
final Selection? initialSelection;
DocumentPluginWidgetBuilder({
required this.notifier,
Key? key,
});
@override @override
EdgeInsets get contentPadding => EdgeInsets.zero; EdgeInsets get contentPadding => EdgeInsets.zero;
@ -86,21 +92,23 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
@override @override
Widget buildWidget({PluginContext? context, required bool shrinkWrap}) { Widget buildWidget({PluginContext? context, required bool shrinkWrap}) {
notifier.isDeleted.addListener(() { notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) { notifier.isDeleted.value.fold(
() => null,
(deletedView) {
if (deletedView.hasIndex()) { if (deletedView.hasIndex()) {
deletedViewIndex = deletedView.index; deletedViewIndex = deletedView.index;
} }
}); },
);
}); });
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>( return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
builder: (_, state) { builder: (_, state) => DocumentPage(
return DocumentPage( key: ValueKey(view.id),
view: view, view: view,
onDeleted: () => context?.onDeleted(view, deletedViewIndex), onDeleted: () => context?.onDeleted(view, deletedViewIndex),
key: ValueKey(view.id), initialSelection: initialSelection,
); ),
},
); );
} }
@ -114,10 +122,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
Widget? get rightBarItem { Widget? get rightBarItem {
return Row( return Row(
children: [ children: [
DocumentShareButton( DocumentShareButton(key: ValueKey(view.id), view: view),
key: ValueKey(view.id),
view: view,
),
const HSpace(4), const HSpace(4),
const DocumentMoreButton(), const DocumentMoreButton(),
], ],

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart';
@ -13,7 +15,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
enum EditorNotificationType { enum EditorNotificationType {
@ -35,12 +36,14 @@ class EditorNotification extends Notification {
class DocumentPage extends StatefulWidget { class DocumentPage extends StatefulWidget {
const DocumentPage({ const DocumentPage({
super.key, super.key,
required this.onDeleted,
required this.view, required this.view,
required this.onDeleted,
this.initialSelection,
}); });
final VoidCallback onDeleted;
final ViewPB view; final ViewPB view;
final VoidCallback onDeleted;
final Selection? initialSelection;
@override @override
State<DocumentPage> createState() => _DocumentPageState(); State<DocumentPage> createState() => _DocumentPageState();
@ -88,10 +91,8 @@ class _DocumentPageState extends State<DocumentPage> {
return BlocListener<NotificationActionBloc, NotificationActionState>( return BlocListener<NotificationActionBloc, NotificationActionState>(
listener: _onNotificationAction, listener: _onNotificationAction,
child: _buildEditorPage( listenWhen: (_, curr) => curr.action != null,
context, child: _buildEditorPage(context, state),
state,
),
); );
}, },
), ),
@ -107,6 +108,7 @@ class _DocumentPageState extends State<DocumentPage> {
padding: EditorStyleCustomizer.documentPadding, padding: EditorStyleCustomizer.documentPadding,
), ),
header: _buildCoverAndIcon(context, state.editorState!), header: _buildCoverAndIcon(context, state.editorState!),
initialSelection: widget.initialSelection,
); );
return Column( return Column(
@ -167,14 +169,12 @@ class _DocumentPageState extends State<DocumentPage> {
NotificationActionState state, NotificationActionState state,
) async { ) async {
if (state.action != null && state.action!.type == ActionType.jumpToBlock) { if (state.action != null && state.action!.type == ActionType.jumpToBlock) {
final path = state.action?.arguments?[ActionArgumentKeys.nodePath.name]; final path = state.action?.arguments?[ActionArgumentKeys.nodePath];
final editorState = context.read<DocumentBloc>().state.editorState; final editorState = context.read<DocumentBloc>().state.editorState;
if (editorState != null && widget.view.id == state.action?.objectId) { if (editorState != null && widget.view.id == state.action?.objectId) {
editorState.updateSelectionWithReason( editorState.updateSelectionWithReason(
Selection.collapsed( Selection.collapsed(Position(path: [path])),
Position(path: [path]),
),
reason: SelectionUpdateReason.transaction, reason: SelectionUpdateReason.transaction,
); );
} }

View File

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart';
@ -12,9 +15,6 @@ import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.d
import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart'; import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
@ -22,8 +22,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
final List<CommandShortcutEvent> commandShortcutEvents = [ final List<CommandShortcutEvent> commandShortcutEvents = [
@ -52,6 +50,7 @@ class AppFlowyEditorPage extends StatefulWidget {
required this.styleCustomizer, required this.styleCustomizer,
this.showParagraphPlaceholder, this.showParagraphPlaceholder,
this.placeholderText, this.placeholderText,
this.initialSelection,
}); });
final Widget? header; final Widget? header;
@ -63,6 +62,10 @@ class AppFlowyEditorPage extends StatefulWidget {
final ShowPlaceholder? showParagraphPlaceholder; final ShowPlaceholder? showParagraphPlaceholder;
final String Function(Node)? placeholderText; final String Function(Node)? placeholderText;
/// Used to provide an initial selection on Page-load
///
final Selection? initialSelection;
@override @override
State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState(); State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
} }
@ -97,13 +100,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
...headingItems ...headingItems
..forEach( ..forEach((e) => e.isActive = onlyShowInSingleSelectionAndTextType),
(e) => e.isActive = onlyShowInSingleSelectionAndTextType, ...markdownFormatItems..forEach((e) => e.isActive = showInAnyTextType),
),
...markdownFormatItems
..forEach(
(e) => e.isActive = showInAnyTextType,
),
quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
bulletedListItem bulletedListItem
..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
@ -177,14 +175,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
late final EditorScrollController editorScrollController; late final EditorScrollController editorScrollController;
Future<bool> showSlashMenu(editorState) async { Future<bool> showSlashMenu(editorState) async => await customSlashCommand(
final result = await customSlashCommand(
slashMenuItems, slashMenuItems,
shouldInsertSlash: false, shouldInsertSlash: false,
style: styleCustomizer.selectionMenuStyleBuilder(), style: styleCustomizer.selectionMenuStyleBuilder(),
).handler(editorState); ).handler(editorState);
return result;
}
@override @override
void initState() { void initState() {
@ -216,6 +211,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
// customize the dynamic theme color // customize the dynamic theme color
_customizeBlockComponentBackgroundColorDecorator(); _customizeBlockComponentBackgroundColorDecorator();
if (widget.initialSelection != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.editorState.updateSelectionWithReason(
widget.initialSelection,
reason: SelectionUpdateReason.transaction,
);
});
}
} }
@override @override
@ -275,7 +279,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
); );
final editorState = widget.editorState; final editorState = widget.editorState;
_setInitialSelection(editorScrollController);
if (PlatformExtension.isMobile) { if (PlatformExtension.isMobile) {
return AppFlowyMobileToolbar( return AppFlowyMobileToolbar(
@ -337,21 +340,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
); );
} }
void _setInitialSelection(EditorScrollController scrollController) {
final action = getIt<NotificationActionBloc>().state.action;
final viewId = action?.objectId;
final nodePath =
action?.arguments?[ActionArgumentKeys.nodePath.name] as int?;
if (viewId != null && viewId == documentBloc.view.id && nodePath != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.itemScrollController.jumpTo(index: nodePath);
widget.editorState.selection =
Selection.collapsed(Position(path: [nodePath]));
});
}
}
List<SelectionMenuItem> _customSlashMenuItems() { List<SelectionMenuItem> _customSlashMenuItems() {
final items = [...standardSelectionMenuItems]; final items = [...standardSelectionMenuItems];
final imageItem = items.firstWhereOrNull( final imageItem = items.firstWhereOrNull(
@ -387,9 +375,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
if (widget.editorState.document.isEmpty) { if (widget.editorState.document.isEmpty) {
return ( return (
true, true,
Selection.collapsed( Selection.collapsed(Position(path: [0], offset: 0)),
Position(path: [0], offset: 0),
),
); );
} }
final nodes = widget.editorState.document.root.children final nodes = widget.editorState.document.root.children
@ -399,9 +385,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
if (isAllEmpty) { if (isAllEmpty) {
return ( return (
true, true,
Selection.collapsed( Selection.collapsed(Position(path: nodes.first.path, offset: 0))
Position(path: nodes.first.path, offset: 0),
)
); );
} }
return const (false, null); return const (false, null);
@ -421,9 +405,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
void _setRTLToolbarItems(bool isRTL) { void _setRTLToolbarItems(bool isRTL) {
final textDirectionItemIds = textDirectionItems.map((e) => e.id); final textDirectionItemIds = textDirectionItems.map((e) => e.id);
// clear all the text direction items // clear all the text direction items
toolbarItems.removeWhere( toolbarItems.removeWhere((item) => textDirectionItemIds.contains(item.id));
(item) => textDirectionItemIds.contains(item.id),
);
// only show the rtl item when the layout direction is ltr. // only show the rtl item when the layout direction is ltr.
if (isRTL) { if (isRTL) {
toolbarItems.addAll(textDirectionItems); toolbarItems.addAll(textDirectionItems);
@ -441,8 +423,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
style, style,
showReplaceMenu, showReplaceMenu,
onDismiss, onDismiss,
) { ) =>
return Material( Material(
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant, color: Theme.of(context).colorScheme.surfaceVariant,
@ -453,8 +435,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
onDismiss: onDismiss, onDismiss: onDismiss,
), ),
), ),
); ),
},
), ),
); );
} }
@ -468,6 +449,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
if (tintColor != null) { if (tintColor != null) {
return tintColor.color(context); return tintColor.color(context);
} }
final themeColor = themeBackgroundColors[colorString]; final themeColor = themeBackgroundColors[colorString];
if (themeColor != null) { if (themeColor != null) {
return themeColor.color(context); return themeColor.color(context);
@ -488,9 +470,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
}; };
} }
void _initEditorL10n() { void _initEditorL10n() => AppFlowyEditorL10n.current = EditorI18n();
AppFlowyEditorL10n.current = EditorI18n();
}
Future<void> _focusOnLastEmptyParagraph() async { Future<void> _focusOnLastEmptyParagraph() async {
final editorState = widget.editorState; final editorState = widget.editorState;
@ -518,6 +498,7 @@ bool showInAnyTextType(EditorState editorState) {
if (selection == null) { if (selection == null) {
return false; return false;
} }
final nodes = editorState.getNodesInSelection(selection); final nodes = editorState.getNodesInSelection(selection);
return nodes.any( return nodes.any(
(node) => toolbarItemWhiteList.contains(node.type), (node) => toolbarItemWhiteList.contains(node.type),

View File

@ -1,39 +1,56 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:collection/collection.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
enum MentionType { enum MentionType {
page, page,
date, reminder,
reminder; date;
static MentionType fromString(String value) { static MentionType fromString(String value) => switch (value) {
switch (value) { 'page' => page,
case 'page': 'date' => date,
return page; // Backwards compatibility
case 'date': 'reminder' => date,
return date; _ => throw UnimplementedError(),
case 'reminder': };
return reminder; }
default:
throw UnimplementedError(); Node dateMentionNode() {
} return paragraphNode(
} delta: Delta(
operations: [
TextInsert(
'\$',
attributes: {
MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionType.date.name,
MentionBlockKeys.date: DateTime.now().toIso8601String(),
},
},
),
],
),
);
} }
class MentionBlockKeys { class MentionBlockKeys {
const MentionBlockKeys._(); const MentionBlockKeys._();
static const uid = 'uid'; // UniqueID static const reminderId = 'reminder_id'; // ReminderID
static const mention = 'mention'; static const mention = 'mention';
static const type = 'type'; // MentionType, String static const type = 'type'; // MentionType, String
static const pageId = 'page_id'; static const pageId = 'page_id';
// Related to Reminder and Date blocks // Related to Reminder and Date blocks
static const date = 'date'; static const date = 'date'; // Start Date
static const includeTime = 'include_time'; static const includeTime = 'include_time';
static const reminderOption = 'reminder_option';
} }
class MentionBlock extends StatelessWidget { class MentionBlock extends StatelessWidget {
@ -62,21 +79,21 @@ class MentionBlock extends StatelessWidget {
pageId: pageId, pageId: pageId,
textStyle: textStyle, textStyle: textStyle,
); );
case MentionType.reminder:
case MentionType.date: case MentionType.date:
final String date = mention[MentionBlockKeys.date]; final String date = mention[MentionBlockKeys.date];
final BuildContext editorContext = final editorState = context.read<EditorState>();
context.read<EditorState>().document.root.context!; final reminderOption = ReminderOption.values.firstWhereOrNull(
(o) => o.name == mention[MentionBlockKeys.reminderOption],
);
return MentionDateBlock( return MentionDateBlock(
key: ValueKey(date), key: ValueKey(date),
editorContext: editorContext, editorState: editorState,
date: date, date: date,
node: node, node: node,
index: index, index: index,
isReminder: type == MentionType.reminder, reminderId: mention[MentionBlockKeys.reminderId],
reminderId: type == MentionType.reminder reminderOption: reminderOption,
? mention[MentionBlockKeys.uid]
: null,
includeTime: mention[MentionBlockKeys.includeTime] ?? false, includeTime: mention[MentionBlockKeys.includeTime] ?? false,
); );
default: default:

View File

@ -1,45 +1,57 @@
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:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.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.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nanoid/non_secure.dart';
class MentionDateBlock extends StatefulWidget { class MentionDateBlock extends StatefulWidget {
const MentionDateBlock({ const MentionDateBlock({
super.key, super.key,
required this.editorContext, required this.editorState,
required this.date, required this.date,
required this.index, required this.index,
required this.node, required this.node,
this.isReminder = false,
this.reminderId, this.reminderId,
this.reminderOption,
this.includeTime = false, this.includeTime = false,
}); });
final BuildContext editorContext; final EditorState editorState;
final String date; final String date;
final int index; final int index;
final Node node; final Node node;
final bool isReminder;
/// If [isReminder] is true, then this must not be /// If [isReminder] is true, then this must not be
/// null or empty /// null or empty
final String? reminderId; final String? reminderId;
final ReminderOption? reminderOption;
final bool includeTime; final bool includeTime;
@override @override
@ -47,14 +59,13 @@ class MentionDateBlock extends StatefulWidget {
} }
class _MentionDateBlockState extends State<MentionDateBlock> { class _MentionDateBlockState extends State<MentionDateBlock> {
late bool includeTime = widget.includeTime;
final PopoverMutex mutex = PopoverMutex(); final PopoverMutex mutex = PopoverMutex();
late bool _includeTime = widget.includeTime;
late DateTime? parsedDate = DateTime.tryParse(widget.date);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final editorState = context.read<EditorState>();
DateTime? parsedDate = DateTime.tryParse(widget.date);
if (parsedDate == null) { if (parsedDate == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -77,10 +88,9 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
builder: (context, state) { builder: (context, state) {
final reminder = state.reminders final reminder = state.reminders
.firstWhereOrNull((r) => r.id == widget.reminderId); .firstWhereOrNull((r) => r.id == widget.reminderId);
final noReminder = reminder == null && widget.isReminder;
final formattedDate = appearance.dateFormat final formattedDate = appearance.dateFormat
.formatDate(parsedDate!, includeTime, appearance.timeFormat); .formatDate(parsedDate!, _includeTime, appearance.timeFormat);
final timeStr = parsedDate != null final timeStr = parsedDate != null
? _timeFromDate(parsedDate!, appearance.timeFormat) ? _timeFromDate(parsedDate!, appearance.timeFormat)
@ -90,28 +100,25 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
focusedDay: parsedDate, focusedDay: parsedDate,
popoverMutex: mutex, popoverMutex: mutex,
selectedDay: parsedDate, selectedDay: parsedDate,
firstDay: widget.isReminder
? noReminder
? parsedDate
: DateTime.now()
: null,
lastDay: noReminder ? parsedDate : null,
timeStr: timeStr, timeStr: timeStr,
includeTime: includeTime, includeTime: _includeTime,
enableRanges: false,
dateFormat: appearance.dateFormat, dateFormat: appearance.dateFormat,
timeFormat: appearance.timeFormat, timeFormat: appearance.timeFormat,
enableRanges: true,
selectedReminderOption: widget.reminderOption,
onIncludeTimeChanged: (includeTime) { onIncludeTimeChanged: (includeTime) {
this.includeTime = includeTime; _includeTime = includeTime;
_updateBlock(parsedDate!.withoutTime, includeTime);
// We can remove time from the date/reminder if (![null, ReminderOption.none]
// block when toggled off. .contains(widget.reminderOption)) {
if (widget.isReminder) { _updateReminder(
_updateScheduledAt( widget.reminderOption!,
reminderId: widget.reminderId!, reminder,
selectedDay: includeTime,
includeTime ? parsedDate! : parsedDate!.withoutTime, );
} else {
_updateBlock(
parsedDate!.withoutTime,
includeTime: includeTime, includeTime: includeTime,
); );
} }
@ -121,37 +128,100 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
parsedDate = parsedDate!.withoutTime parsedDate = parsedDate!.withoutTime
.add(Duration(hours: parsed.hour, minutes: parsed.minute)); .add(Duration(hours: parsed.hour, minutes: parsed.minute));
_updateBlock(parsedDate!, includeTime); if (![null, ReminderOption.none]
.contains(widget.reminderOption)) {
if (widget.isReminder && _updateReminder(
widget.date != parsedDate!.toIso8601String()) { widget.reminderOption!,
_updateScheduledAt( reminder,
reminderId: widget.reminderId!, _includeTime,
selectedDay: parsedDate!,
); );
} else {
_updateBlock(parsedDate!, includeTime: _includeTime);
} }
}, },
onDaySelected: (selectedDay, focusedDay) { onDaySelected: (selectedDay, focusedDay) {
parsedDate = selectedDay; parsedDate = selectedDay;
_updateBlock(selectedDay, includeTime);
if (widget.isReminder && if (![null, ReminderOption.none]
widget.date != selectedDay.toIso8601String()) { .contains(widget.reminderOption)) {
_updateScheduledAt( _updateReminder(
reminderId: widget.reminderId!, widget.reminderOption!,
selectedDay: selectedDay, reminder,
_includeTime,
); );
} else {
_updateBlock(selectedDay, includeTime: _includeTime);
} }
}, },
onReminderSelected: (reminderOption) =>
_updateReminder(reminderOption, reminder),
); );
return GestureDetector( return GestureDetector(
onTapDown: editorState.editable onTapDown: (details) {
? (details) => DatePickerMenu( if (widget.editorState.editable) {
context: context, if (PlatformExtension.isMobile) {
editorState: context.read<EditorState>(), showMobileBottomSheet(
).show(details.globalPosition, options: options) context,
resizeToAvoidBottomInset: false,
builder: (_) => DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: 0.7,
minChildSize: 0.4,
snapSizes: const [0.4, 0.7, 1.0],
builder: (_, controller) => Material(
color:
Theme.of(context).colorScheme.secondaryContainer,
child: ListView(
controller: controller,
children: [
ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: const Center(child: DragHandler()),
),
const MobileDateHeader(),
MobileAppFlowyDatePicker(
selectedDay: parsedDate,
timeStr: timeStr,
dateStr: parsedDate != null
? options.dateFormat
.formatDate(parsedDate!, _includeTime)
: null, : null,
includeTime: options.includeTime,
use24hFormat: options.timeFormat ==
UserTimeFormatPB.TwentyFourHour,
rebuildOnDaySelected: true,
rebuildOnTimeChanged: true,
selectedReminderOption: widget.reminderOption,
onDaySelected: options.onDaySelected,
onStartTimeChanged: (time) => options
.onStartTimeChanged
?.call(time ?? ""),
onIncludeTimeChanged:
options.onIncludeTimeChanged,
liveDateFormatter: (selected) =>
appearance.dateFormat.formatDate(
selected,
false,
appearance.timeFormat,
),
onReminderSelected: (option) =>
_updateReminder(option, reminder),
),
],
),
),
),
);
} else {
DatePickerMenu(
context: context,
editorState: widget.editorState,
).show(details.globalPosition, options: options);
}
}
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: MouseRegion( child: MouseRegion(
@ -160,11 +230,11 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
FlowySvg( FlowySvg(
widget.isReminder widget.reminderId != null
? FlowySvgs.clock_alarm_s ? FlowySvgs.clock_alarm_s
: FlowySvgs.date_s, : FlowySvgs.date_s,
size: const Size.square(18.0), size: const Size.square(18.0),
color: widget.isReminder && reminder?.isAck == true color: reminder?.isAck == true
? Theme.of(context).colorScheme.error ? Theme.of(context).colorScheme.error
: null, : null,
), ),
@ -172,7 +242,7 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
FlowyText( FlowyText(
formattedDate, formattedDate,
fontSize: fontSize, fontSize: fontSize,
color: widget.isReminder && reminder?.isAck == true color: reminder?.isAck == true
? Theme.of(context).colorScheme.error ? Theme.of(context).colorScheme.error
: null, : null,
), ),
@ -191,11 +261,16 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
final twelveHourFormat = DateFormat('HH:mm a'); final twelveHourFormat = DateFormat('HH:mm a');
final twentyFourHourFormat = DateFormat('HH:mm'); final twentyFourHourFormat = DateFormat('HH:mm');
try {
if (timeFormat == TimeFormatPB.TwelveHour) { if (timeFormat == TimeFormatPB.TwelveHour) {
return twelveHourFormat.parse(timeStr); return twelveHourFormat.parse(timeStr);
} }
return twentyFourHourFormat.parse(timeStr); return twentyFourHourFormat.parse(timeStr);
} on FormatException {
Log.error("failed to parse time string ($timeStr)");
return DateTime.now();
}
} }
String _timeFromDate(DateTime date, UserTimeFormatPB timeFormat) { String _timeFromDate(DateTime date, UserTimeFormatPB timeFormat) {
@ -210,43 +285,94 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
} }
void _updateBlock( void _updateBlock(
DateTime date, [ DateTime date, {
bool includeTime = false, bool includeTime = false,
]) { String? reminderId,
final editorState = widget.editorContext.read<EditorState>(); ReminderOption? reminderOption,
final transaction = editorState.transaction }) {
final rId = reminderId ??
(reminderOption == ReminderOption.none ? null : widget.reminderId);
final transaction = widget.editorState.transaction
..formatText(widget.node, widget.index, 1, { ..formatText(widget.node, widget.index, 1, {
MentionBlockKeys.mention: { MentionBlockKeys.mention: {
MentionBlockKeys.type: widget.isReminder MentionBlockKeys.type: MentionType.date.name,
? MentionType.reminder.name
: MentionType.date.name,
MentionBlockKeys.date: date.toIso8601String(), MentionBlockKeys.date: date.toIso8601String(),
MentionBlockKeys.uid: widget.reminderId, MentionBlockKeys.reminderId: rId,
MentionBlockKeys.includeTime: includeTime, MentionBlockKeys.includeTime: includeTime,
MentionBlockKeys.reminderOption:
reminderOption?.name ?? widget.reminderOption?.name,
}, },
}); });
editorState.apply(transaction, withUpdateSelection: false); widget.editorState.apply(transaction, withUpdateSelection: false);
// Length of rendered block changes, this synchronizes // Length of rendered block changes, this synchronizes
// the cursor with the new block render // the cursor with the new block render
editorState.updateSelectionWithReason( widget.editorState.updateSelectionWithReason(
editorState.selection, widget.editorState.selection,
reason: SelectionUpdateReason.transaction, reason: SelectionUpdateReason.transaction,
); );
} }
void _updateScheduledAt({ void _updateReminder(
required String reminderId, ReminderOption reminderOption,
required DateTime selectedDay, ReminderPB? reminder, [
bool? includeTime, bool includeTime = false,
}) { ]) {
widget.editorContext.read<ReminderBloc>().add( final rootContext = widget.editorState.document.root.context;
if (parsedDate == null || rootContext == null) {
return;
}
if (widget.reminderId != null) {
_updateBlock(
parsedDate!,
includeTime: includeTime,
reminderOption: reminderOption,
);
if (ReminderOption.none == reminderOption && reminder != null) {
// Delete existing reminder
return rootContext
.read<ReminderBloc>()
.add(ReminderEvent.remove(reminderId: reminder.id));
}
// Update existing reminder
return rootContext.read<ReminderBloc>().add(
ReminderEvent.update( ReminderEvent.update(
ReminderUpdate( ReminderUpdate(
id: reminderId, id: widget.reminderId!,
scheduledAt: selectedDay, scheduledAt: parsedDate!.subtract(reminderOption.time),
),
),
);
}
final reminderId = nanoid();
_updateBlock(
parsedDate!,
includeTime: includeTime, includeTime: includeTime,
reminderId: reminderId,
reminderOption: reminderOption,
);
// Add new reminder
final viewId = rootContext.read<DocumentBloc>().view.id;
return rootContext.read<ReminderBloc>().add(
ReminderEvent.add(
reminder: ReminderPB(
id: reminderId,
objectId: viewId,
title: LocaleKeys.reminderNotification_title.tr(),
message: LocaleKeys.reminderNotification_message.tr(),
meta: {
ReminderMetaKeys.includeTime: false.toString(),
ReminderMetaKeys.blockId: widget.node.id,
},
scheduledAt: Int64(parsedDate!.millisecondsSinceEpoch ~/ 1000),
isAck: parsedDate!.isBefore(DateTime.now()),
), ),
), ),
); );

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
@ -9,7 +12,6 @@ import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
final addBlockToolbarItem = AppFlowyMobileToolbarItem( final addBlockToolbarItem = AppFlowyMobileToolbarItem(
@ -213,6 +215,15 @@ class _AddBlockMenu extends StatelessWidget {
}); });
}, },
), ),
// date
_AddBlockMenuItemData(
blockType: ParagraphBlockKeys.type,
backgroundColor: const Color(0xFFF49898),
text: LocaleKeys.editor_date.tr(),
icon: FlowySvgs.date_s,
onTap: () => _insertBlock(dateMentionNode()),
),
]; ];
@override @override

View File

@ -252,9 +252,7 @@ class EditorStyleCustomizer {
key: ValueKey( key: ValueKey(
switch (type) { switch (type) {
MentionType.page => mention[MentionBlockKeys.pageId], MentionType.page => mention[MentionBlockKeys.pageId],
MentionType.date || MentionType.date => mention[MentionBlockKeys.date],
MentionType.reminder =>
mention[MentionBlockKeys.date],
_ => MentionBlockKeys.mention, _ => MentionBlockKeys.mention,
}, },
), ),

View File

@ -1,16 +1,18 @@
import 'package:flutter/material.dart';
import 'package:appflowy/date/date_service.dart'; import 'package:appflowy/date/date_service.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nanoid/nanoid.dart'; import 'package:nanoid/nanoid.dart';
@ -147,9 +149,10 @@ class ReminderReferenceService {
'\$', '\$',
attributes: { attributes: {
MentionBlockKeys.mention: { MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionType.reminder.name, MentionBlockKeys.type: MentionType.date.name,
MentionBlockKeys.date: date.toIso8601String(), MentionBlockKeys.date: date.toIso8601String(),
MentionBlockKeys.uid: reminder.id, MentionBlockKeys.reminderId: reminder.id,
MentionBlockKeys.reminderOption: ReminderOption.atTimeOfEvent.name,
}, },
}, },
); );
@ -213,8 +216,8 @@ class ReminderReferenceService {
title: LocaleKeys.reminderNotification_title.tr(), title: LocaleKeys.reminderNotification_title.tr(),
message: LocaleKeys.reminderNotification_message.tr(), message: LocaleKeys.reminderNotification_message.tr(),
meta: { meta: {
ReminderMetaKeys.includeTime.name: false.toString(), ReminderMetaKeys.includeTime: false.toString(),
ReminderMetaKeys.blockId.name: node.id, ReminderMetaKeys.blockId: node.id,
}, },
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000), scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
isAck: date.isBefore(DateTime.now()), isAck: date.isBefore(DateTime.now()),

View File

@ -1,18 +1,22 @@
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/notifications/notification_service.dart'; import 'package:appflowy/workspace/application/notifications/notification_service.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -140,30 +144,33 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
create: (_) => DocumentAppearanceCubit()..fetch(), create: (_) => DocumentAppearanceCubit()..fetch(),
), ),
BlocProvider.value(value: getIt<NotificationActionBloc>()), BlocProvider.value(value: getIt<NotificationActionBloc>()),
BlocProvider.value(
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
),
], ],
child: BlocListener<NotificationActionBloc, NotificationActionState>( child: BlocListener<NotificationActionBloc, NotificationActionState>(
listenWhen: (_, curr) => curr.action != null,
listener: (context, state) { listener: (context, state) {
if (state.action?.type == ActionType.openView) { final action = state.action;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final view = if (action?.type == ActionType.openView &&
state.action!.arguments?[ActionArgumentKeys.view.name]; PlatformExtension.isDesktop) {
final view = action!.arguments?[ActionArgumentKeys.view];
if (view != null) { if (view != null) {
AppGlobals.rootNavKey.currentContext?.pushView(view); AppGlobals.rootNavKey.currentContext?.pushView(view);
}
final nodePath = state.action! } else if (action?.type == ActionType.openRow &&
.arguments?[ActionArgumentKeys.nodePath.name] as int?; PlatformExtension.isMobile) {
final view = action!.arguments?[ActionArgumentKeys.view];
if (nodePath != null) { if (view != null) {
context.read<NotificationActionBloc>().add( final view = action.arguments?[ActionArgumentKeys.view];
NotificationActionEvent.performAction( final rowId = action.arguments?[ActionArgumentKeys.rowId];
action: state.action! AppGlobals.rootNavKey.currentContext?.pushView(view, {
.copyWith(type: ActionType.jumpToBlock), PluginArgumentKeys.rowId: rowId,
), });
);
} }
} }
}); });
}
}, },
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>( child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) => MaterialApp.router( builder: (context, state) => MaterialApp.router(

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart'; import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
@ -489,10 +491,13 @@ GoRoute _mobileGridScreenRoute() {
pageBuilder: (context, state) { pageBuilder: (context, state) {
final id = state.uri.queryParameters[MobileGridScreen.viewId]!; final id = state.uri.queryParameters[MobileGridScreen.viewId]!;
final title = state.uri.queryParameters[MobileGridScreen.viewTitle]; final title = state.uri.queryParameters[MobileGridScreen.viewTitle];
final arguments = state.uri.queryParameters[MobileGridScreen.viewArgs];
return MaterialPage( return MaterialPage(
child: MobileGridScreen( child: MobileGridScreen(
id: id, id: id,
title: title, title: title,
arguments: arguments != null ? jsonDecode(arguments) : null,
), ),
); );
}, },

View File

@ -5,6 +5,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/user/application/reminder/reminder_service.dart'; import 'package:appflowy/user/application/reminder/reminder_service.dart';
import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/util/int64_extension.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/notifications/notification_service.dart'; import 'package:appflowy/workspace/application/notifications/notification_service.dart';
@ -15,19 +16,18 @@ import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'reminder_bloc.freezed.dart'; part 'reminder_bloc.freezed.dart';
class ReminderBloc extends Bloc<ReminderEvent, ReminderState> { class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
late final NotificationActionBloc actionBloc; late final NotificationActionBloc _actionBloc;
late final ReminderService reminderService; late final ReminderService _reminderService;
late final Timer timer; late final Timer timer;
ReminderBloc() : super(ReminderState()) { ReminderBloc() : super(ReminderState()) {
actionBloc = getIt<NotificationActionBloc>(); _actionBloc = getIt<NotificationActionBloc>();
reminderService = const ReminderService(); _reminderService = const ReminderService();
timer = _periodicCheck(); timer = _periodicCheck();
on<ReminderEvent>((event, emit) async { on<ReminderEvent>((event, emit) async {
@ -42,7 +42,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
reminders.remove(reminder); reminders.remove(reminder);
reminder.isRead = true; reminder.isRead = true;
await reminderService.updateReminder(reminder: reminder); await _reminderService.updateReminder(reminder: reminder);
updatedReminders.add(reminder); updatedReminders.add(reminder);
} }
@ -51,29 +51,29 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
emit(state.copyWith(reminders: reminders)); emit(state.copyWith(reminders: reminders));
}, },
started: () async { started: () async {
final remindersOrFailure = await reminderService.fetchReminders(); final remindersOrFailure = await _reminderService.fetchReminders();
remindersOrFailure.fold( remindersOrFailure.fold(
(error) => Log.error(error), (error) => Log.error(error),
(reminders) => emit(state.copyWith(reminders: reminders)), (reminders) => emit(state.copyWith(reminders: reminders)),
); );
}, },
remove: (reminder) async { remove: (reminderId) async {
final unitOrFailure = final unitOrFailure =
await reminderService.removeReminder(reminderId: reminder.id); await _reminderService.removeReminder(reminderId: reminderId);
unitOrFailure.fold( unitOrFailure.fold(
(error) => Log.error(error), (error) => Log.error(error),
(_) { (_) {
final reminders = [...state.reminders]; final reminders = [...state.reminders];
reminders.removeWhere((e) => e.id == reminder.id); reminders.removeWhere((e) => e.id == reminderId);
emit(state.copyWith(reminders: reminders)); emit(state.copyWith(reminders: reminders));
}, },
); );
}, },
add: (reminder) async { add: (reminder) async {
final unitOrFailure = final unitOrFailure =
await reminderService.addReminder(reminder: reminder); await _reminderService.addReminder(reminder: reminder);
return unitOrFailure.fold( return unitOrFailure.fold(
(error) => Log.error(error), (error) => Log.error(error),
@ -83,6 +83,19 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
}, },
); );
}, },
addById: (reminderId, objectId, scheduledAt, meta) async => add(
ReminderEvent.add(
reminder: ReminderPB(
id: reminderId,
objectId: objectId,
title: LocaleKeys.reminderNotification_title.tr(),
message: LocaleKeys.reminderNotification_message.tr(),
scheduledAt: scheduledAt,
isAck: scheduledAt.toDateTime().isBefore(DateTime.now()),
meta: meta,
),
),
),
update: (updateObject) async { update: (updateObject) async {
final reminder = final reminder =
state.reminders.firstWhereOrNull((r) => r.id == updateObject.id); state.reminders.firstWhereOrNull((r) => r.id == updateObject.id);
@ -92,7 +105,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
} }
final newReminder = updateObject.merge(a: reminder); final newReminder = updateObject.merge(a: reminder);
final failureOrUnit = await reminderService.updateReminder( final failureOrUnit = await _reminderService.updateReminder(
reminder: updateObject.merge(a: reminder), reminder: updateObject.merge(a: reminder),
); );
@ -124,17 +137,34 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
), ),
); );
actionBloc.add( String? rowId;
NotificationActionEvent.performAction( if (view?.layout != ViewLayoutPB.Document) {
action: NotificationAction( rowId = reminder.meta[ReminderMetaKeys.rowId];
}
final action = NotificationAction(
objectId: reminder.objectId, objectId: reminder.objectId,
arguments: { arguments: {
ActionArgumentKeys.nodePath.name: path, ActionArgumentKeys.view: view,
ActionArgumentKeys.view.name: view, ActionArgumentKeys.nodePath: path,
ActionArgumentKeys.rowId: rowId,
}, },
);
if (!isClosed) {
_actionBloc.add(
NotificationActionEvent.performAction(
action: action,
nextActions: [
action.copyWith(
type: rowId != null
? ActionType.openRow
: ActionType.jumpToBlock,
), ),
],
), ),
); );
}
}, },
); );
}); });
@ -151,9 +181,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
continue; continue;
} }
final scheduledAt = DateTime.fromMillisecondsSinceEpoch( final scheduledAt = reminder.scheduledAt.toDateTime();
reminder.scheduledAt.toInt() * 1000,
);
if (scheduledAt.isBefore(now)) { if (scheduledAt.isBefore(now)) {
final notificationSettings = final notificationSettings =
@ -163,7 +191,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
identifier: reminder.id, identifier: reminder.id,
title: LocaleKeys.reminderNotification_title.tr(), title: LocaleKeys.reminderNotification_title.tr(),
body: LocaleKeys.reminderNotification_message.tr(), body: LocaleKeys.reminderNotification_message.tr(),
onClick: () => actionBloc.add( onClick: () => _actionBloc.add(
NotificationActionEvent.performAction( NotificationActionEvent.performAction(
action: NotificationAction(objectId: reminder.objectId), action: NotificationAction(objectId: reminder.objectId),
), ),
@ -189,11 +217,19 @@ class ReminderEvent with _$ReminderEvent {
const factory ReminderEvent.started() = _Started; const factory ReminderEvent.started() = _Started;
// Remove a reminder // Remove a reminder
const factory ReminderEvent.remove({required ReminderPB reminder}) = _Remove; const factory ReminderEvent.remove({required String reminderId}) = _Remove;
// Add a reminder // Add a reminder
const factory ReminderEvent.add({required ReminderPB reminder}) = _Add; const factory ReminderEvent.add({required ReminderPB reminder}) = _Add;
// Add a reminder
const factory ReminderEvent.addById({
required String reminderId,
required String objectId,
required Int64 scheduledAt,
@Default(null) Map<String, String>? meta,
}) = _AddById;
// Update a reminder (eg. isAck, isRead, etc.) // Update a reminder (eg. isAck, isRead, etc.)
const factory ReminderEvent.update(ReminderUpdate update) = _Update; const factory ReminderEvent.update(ReminderUpdate update) = _Update;
@ -232,7 +268,7 @@ class ReminderUpdate {
final meta = a.meta; final meta = a.meta;
if (includeTime != a.includeTime) { if (includeTime != a.includeTime) {
meta[ReminderMetaKeys.includeTime.name] = includeTime.toString(); meta[ReminderMetaKeys.includeTime] = includeTime.toString();
} }
return ReminderPB( return ReminderPB(

View File

@ -1,17 +1,14 @@
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
enum ReminderMetaKeys { class ReminderMetaKeys {
includeTime("include_time"), static String includeTime = "include_time";
blockId("block_id"); static String blockId = "block_id";
static String rowId = "row_id";
const ReminderMetaKeys(this.name);
final String name;
} }
extension ReminderExtension on ReminderPB { extension ReminderExtension on ReminderPB {
bool? get includeTime { bool? get includeTime {
final String? includeTimeStr = meta[ReminderMetaKeys.includeTime.name]; final String? includeTimeStr = meta[ReminderMetaKeys.includeTime];
return includeTimeStr != null ? includeTimeStr == true.toString() : null; return includeTimeStr != null ? includeTimeStr == true.toString() : null;
} }

View File

@ -0,0 +1,5 @@
import 'package:fixnum/fixnum.dart';
extension DateConversion on Int64 {
DateTime toDateTime() => DateTime.fromMillisecondsSinceEpoch(toInt() * 1000);
}

View File

@ -1,6 +1,13 @@
enum ActionType { enum ActionType {
openView, openView,
jumpToBlock, jumpToBlock,
openRow,
}
class ActionArgumentKeys {
static String view = "view";
static String nodePath = "node_path";
static String rowId = "row_id";
} }
/// A [NotificationAction] is used to communicate with the /// A [NotificationAction] is used to communicate with the
@ -31,12 +38,3 @@ class NotificationAction {
arguments: arguments ?? this.arguments, arguments: arguments ?? this.arguments,
); );
} }
enum ActionArgumentKeys {
view('view'),
nodePath('node_path');
final String name;
const ActionArgumentKeys(this.name);
}

View File

@ -9,8 +9,20 @@ class NotificationActionBloc
NotificationActionBloc() : super(const NotificationActionState.initial()) { NotificationActionBloc() : super(const NotificationActionState.initial()) {
on<NotificationActionEvent>((event, emit) async { on<NotificationActionEvent>((event, emit) async {
event.when( event.when(
performAction: (action) { performAction: (action, nextActions) {
emit(state.copyWith(action: action)); emit(state.copyWith(action: action, nextActions: nextActions));
if (nextActions.isNotEmpty) {
final newActions = [...nextActions];
final next = newActions.removeAt(0);
add(
NotificationActionEvent.performAction(
action: next,
nextActions: newActions,
),
);
}
}, },
); );
}); });
@ -21,18 +33,29 @@ class NotificationActionBloc
class NotificationActionEvent with _$NotificationActionEvent { class NotificationActionEvent with _$NotificationActionEvent {
const factory NotificationActionEvent.performAction({ const factory NotificationActionEvent.performAction({
required NotificationAction action, required NotificationAction action,
@Default([]) List<NotificationAction> nextActions,
}) = _PerformAction; }) = _PerformAction;
} }
class NotificationActionState { class NotificationActionState {
const NotificationActionState({required this.action}); const NotificationActionState({
required this.action,
this.nextActions = const [],
});
final NotificationAction? action; final NotificationAction? action;
final List<NotificationAction> nextActions;
const NotificationActionState.initial() : action = null; const NotificationActionState.initial()
: action = null,
nextActions = const [];
NotificationActionState copyWith({ NotificationActionState copyWith({
NotificationAction? action, NotificationAction? action,
List<NotificationAction>? nextActions,
}) => }) =>
NotificationActionState(action: action ?? this.action); NotificationActionState(
action: action ?? this.action,
nextActions: nextActions ?? this.nextActions,
);
} }

View File

@ -1,3 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
@ -6,7 +8,6 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'tabs_event.dart'; part 'tabs_event.dart';
@ -67,6 +68,14 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
add(TabsEvent.openTab(plugin: view.plugin(), view: view)); add(TabsEvent.openTab(plugin: view.plugin(), view: view));
/// Adds a [TabsEvent.openPlugin] event for the provided [ViewPB] /// Adds a [TabsEvent.openPlugin] event for the provided [ViewPB]
void openPlugin(ViewPB view) => void openPlugin(
add(TabsEvent.openPlugin(plugin: view.plugin(), view: view)); ViewPB view, {
Map<String, dynamic> arguments = const {},
}) =>
add(
TabsEvent.openPlugin(
plugin: view.plugin(arguments: arguments),
view: view,
),
);
} }

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart'; import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
@ -9,14 +11,19 @@ import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:dartz/dartz.dart' hide id; import 'package:dartz/dartz.dart' hide id;
import 'package:flutter/material.dart';
enum FlowyPlugin { enum FlowyPlugin {
editor, editor,
kanban, kanban,
} }
class PluginArgumentKeys {
static String selection = "selection";
static String rowId = "row_id";
}
extension ViewExtension on ViewPB { extension ViewExtension on ViewPB {
Widget defaultIcon() => FlowySvg( Widget defaultIcon() => FlowySvg(
switch (layout) { switch (layout) {
@ -36,17 +43,30 @@ extension ViewExtension on ViewPB {
_ => throw UnimplementedError(), _ => throw UnimplementedError(),
}; };
Plugin plugin({bool listenOnViewChanged = false}) { Plugin plugin({
bool listenOnViewChanged = false,
Map<String, dynamic> arguments = const {},
}) {
switch (layout) { switch (layout) {
case ViewLayoutPB.Board: case ViewLayoutPB.Board:
case ViewLayoutPB.Calendar: case ViewLayoutPB.Calendar:
case ViewLayoutPB.Grid: case ViewLayoutPB.Grid:
return DatabaseTabBarViewPlugin(view: this, pluginType: pluginType); final String? rowId = arguments[PluginArgumentKeys.rowId];
return DatabaseTabBarViewPlugin(
view: this,
pluginType: pluginType,
initialRowId: rowId,
);
case ViewLayoutPB.Document: case ViewLayoutPB.Document:
final Selection? initialSelection =
arguments[PluginArgumentKeys.selection];
return DocumentPlugin( return DocumentPlugin(
view: this, view: this,
pluginType: pluginType, pluginType: pluginType,
listenOnViewChanged: listenOnViewChanged, listenOnViewChanged: listenOnViewChanged,
initialSelection: initialSelection,
); );
} }
throw UnimplementedError; throw UnimplementedError;

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
@ -14,8 +16,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB; show UserProfilePB;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
/// Home Sidebar is the left side bar of the home page. /// Home Sidebar is the left side bar of the home page.
@ -63,6 +65,7 @@ class HomeSideBar extends StatelessWidget {
), ),
), ),
BlocListener<NotificationActionBloc, NotificationActionState>( BlocListener<NotificationActionBloc, NotificationActionState>(
listenWhen: (_, curr) => curr.action != null,
listener: _onNotificationAction, listener: _onNotificationAction,
), ),
], ],
@ -147,17 +150,21 @@ class HomeSideBar extends StatelessWidget {
context.read<MenuBloc>().state.views.findView(action.objectId); context.read<MenuBloc>().state.views.findView(action.objectId);
if (view != null) { if (view != null) {
context.read<TabsBloc>().openPlugin(view); final Map<String, dynamic> arguments = {};
final nodePath = final nodePath = action.arguments?[ActionArgumentKeys.nodePath];
action.arguments?[ActionArgumentKeys.nodePath.name] as int?;
if (nodePath != null) { if (nodePath != null) {
context.read<NotificationActionBloc>().add( arguments[PluginArgumentKeys.selection] = Selection.collapsed(
NotificationActionEvent.performAction( Position(path: [nodePath]),
action: action.copyWith(type: ActionType.jumpToBlock),
),
); );
} }
final rowId = action.arguments?[ActionArgumentKeys.rowId];
if (rowId != null) {
arguments[PluginArgumentKeys.rowId] = rowId;
}
context.read<TabsBloc>().openPlugin(view, arguments: arguments);
} }
} }
} }

View File

@ -117,7 +117,7 @@ class _NotificationDialogState extends State<NotificationDialog>
} }
void _onDelete(ReminderPB reminder) { void _onDelete(ReminderPB reminder) {
_reminderBloc.add(ReminderEvent.remove(reminder: reminder)); _reminderBloc.add(ReminderEvent.remove(reminderId: reminder.id));
} }
void _onReadChanged(ReminderPB reminder, bool isRead) { void _onReadChanged(ReminderPB reminder, bool isRead) {

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
@ -9,9 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
/// Displays a Lsit of Notifications, currently used primarily to /// Displays a Lsit of Notifications, currently used primarily to
/// display Reminders. /// display Reminders.
@ -62,8 +63,7 @@ class NotificationsView extends StatelessWidget {
children: [ children: [
...shownReminders.map( ...shownReminders.map(
(ReminderPB reminder) { (ReminderPB reminder) {
final blockId = final blockId = reminder.meta[ReminderMetaKeys.blockId];
reminder.meta[ReminderMetaKeys.blockId.name];
final documentService = DocumentService(); final documentService = DocumentService();
final documentFuture = documentService.openDocument( final documentFuture = documentService.openDocument(
@ -76,9 +76,7 @@ class NotificationsView extends StatelessWidget {
_getNodeFromDocument(documentFuture, blockId); _getNodeFromDocument(documentFuture, blockId);
} }
final view = views final view = views.findView(reminder.objectId);
.firstWhereOrNull((v) => v.id == reminder.objectId);
return NotificationItem( return NotificationItem(
reminderId: reminder.id, reminderId: reminder.id,
key: ValueKey(reminder.id), key: ValueKey(reminder.id),

View File

@ -1,16 +1,22 @@
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.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_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/end_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/start_text_field.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_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class OptionGroup {
OptionGroup({required this.options});
final List<Widget> options;
}
typedef DaySelectedCallback = Function(DateTime, DateTime); typedef DaySelectedCallback = Function(DateTime, DateTime);
typedef RangeSelectedCallback = Function(DateTime?, DateTime?, DateTime); typedef RangeSelectedCallback = Function(DateTime?, DateTime?, DateTime);
@ -32,20 +38,27 @@ class AppFlowyDatePicker extends StatefulWidget {
this.focusedDay, this.focusedDay,
this.firstDay, this.firstDay,
this.lastDay, this.lastDay,
this.startDay,
this.endDay,
this.timeStr, this.timeStr,
this.endTimeStr, this.endTimeStr,
this.timeHintText, this.timeHintText,
this.parseEndTimeError, this.parseEndTimeError,
this.parseTimeError, this.parseTimeError,
this.popoverMutex, this.popoverMutex,
this.selectedReminderOption = ReminderOption.none,
this.onStartTimeSubmitted, this.onStartTimeSubmitted,
this.onEndTimeSubmitted, this.onEndTimeSubmitted,
this.onDaySelected, this.onDaySelected,
this.onRangeSelected, this.onRangeSelected,
this.onReminderSelected,
this.options,
this.allowFormatChanges = false, this.allowFormatChanges = false,
this.onDateFormatChanged, this.onDateFormatChanged,
this.onTimeFormatChanged, this.onTimeFormatChanged,
this.onClearDate, this.onClearDate,
this.onCalendarCreated,
this.onPageChanged,
}); });
final bool includeTime; final bool includeTime;
@ -64,17 +77,33 @@ class AppFlowyDatePicker extends StatefulWidget {
final DateTime? focusedDay; final DateTime? focusedDay;
final DateTime? firstDay; final DateTime? firstDay;
final DateTime? lastDay; final DateTime? lastDay;
/// Start date in selected range
final DateTime? startDay;
/// End date in selected range
final DateTime? endDay;
final String? timeStr; final String? timeStr;
final String? endTimeStr; final String? endTimeStr;
final String? timeHintText; final String? timeHintText;
final String? parseEndTimeError; final String? parseEndTimeError;
final String? parseTimeError; final String? parseTimeError;
final PopoverMutex? popoverMutex; final PopoverMutex? popoverMutex;
final ReminderOption selectedReminderOption;
final TimeChangedCallback? onStartTimeSubmitted; final TimeChangedCallback? onStartTimeSubmitted;
final TimeChangedCallback? onEndTimeSubmitted; final TimeChangedCallback? onEndTimeSubmitted;
final DaySelectedCallback? onDaySelected; final DaySelectedCallback? onDaySelected;
final RangeSelectedCallback? onRangeSelected; final RangeSelectedCallback? onRangeSelected;
final OnReminderSelected? onReminderSelected;
/// A list of [OptionGroup] that will be rendered with proper
/// separators, each group can contain multiple options.
///
/// __Supported on Desktop & Web__
///
final List<OptionGroup>? options;
/// If this value is true, then [onTimeFormatChanged] and [onDateFormatChanged] /// If this value is true, then [onTimeFormatChanged] and [onDateFormatChanged]
/// cannot be null /// cannot be null
@ -94,21 +123,45 @@ class AppFlowyDatePicker extends StatefulWidget {
/// ///
final VoidCallback? onClearDate; final VoidCallback? onClearDate;
final void Function(PageController pageController)? onCalendarCreated;
final void Function(DateTime focusedDay)? onPageChanged;
@override @override
State<AppFlowyDatePicker> createState() => _AppFlowyDatePickerState(); State<AppFlowyDatePicker> createState() => _AppFlowyDatePickerState();
} }
class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> { class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
late DateTime? _selectedDay = widget.selectedDay; late DateTime? _selectedDay = widget.selectedDay;
late ReminderOption _selectedReminderOption = widget.selectedReminderOption;
@override @override
void didChangeDependencies() { Widget build(BuildContext context) =>
_selectedDay = widget.selectedDay; PlatformExtension.isMobile ? buildMobilePicker() : buildDesktopPicker();
super.didChangeDependencies();
Widget buildMobilePicker() {
return DatePicker(
isRange: widget.isRange,
onDaySelected: (selectedDay, focusedDay) {
widget.onDaySelected?.call(selectedDay, focusedDay);
if (widget.rebuildOnDaySelected) {
setState(() => _selectedDay = selectedDay);
}
},
onRangeSelected: widget.onRangeSelected,
selectedDay:
widget.rebuildOnDaySelected ? _selectedDay : widget.selectedDay,
firstDay: widget.firstDay,
lastDay: widget.lastDay,
startDay: widget.startDay,
endDay: widget.endDay,
onCalendarCreated: widget.onCalendarCreated,
onPageChanged: widget.onPageChanged,
);
} }
@override Widget buildDesktopPicker() {
Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 18.0, bottom: 12.0), padding: const EdgeInsets.only(top: 18.0, bottom: 12.0),
child: Column( child: Column(
@ -142,9 +195,14 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
} }
}, },
onRangeSelected: widget.onRangeSelected, onRangeSelected: widget.onRangeSelected,
selectedDay: _selectedDay, selectedDay:
widget.rebuildOnDaySelected ? _selectedDay : widget.selectedDay,
firstDay: widget.firstDay, firstDay: widget.firstDay,
lastDay: widget.lastDay, lastDay: widget.lastDay,
startDay: widget.startDay,
endDay: widget.endDay,
onCalendarCreated: widget.onCalendarCreated,
onPageChanged: widget.onPageChanged,
), ),
const TypeOptionSeparator(spacing: 12.0), const TypeOptionSeparator(spacing: 12.0),
if (widget.enableRanges && widget.onIsRangeChanged != null) ...[ if (widget.enableRanges && widget.onIsRangeChanged != null) ...[
@ -161,28 +219,44 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
onChanged: widget.onIncludeTimeChanged, onChanged: widget.onIncludeTimeChanged,
), ),
), ),
if (widget.onClearDate != null || const _GroupSeparator(),
(widget.allowFormatChanges && ReminderSelector(
widget.onDateFormatChanged != null && mutex: widget.popoverMutex,
widget.onTimeFormatChanged != null)) selectedOption: _selectedReminderOption,
// Only show if either of the options are below it onOptionSelected: (option) {
const TypeOptionSeparator(spacing: 8.0), setState(() => _selectedReminderOption = option);
if (widget.allowFormatChanges && widget.onReminderSelected?.call(option);
widget.onDateFormatChanged != null && },
widget.onTimeFormatChanged != null) ),
DateTypeOptionButton( if (widget.options?.isNotEmpty ?? false) ...[
popoverMutex: widget.popoverMutex, const _GroupSeparator(),
dateFormat: widget.dateFormat, ListView.separated(
timeFormat: widget.timeFormat, shrinkWrap: true,
onDateFormatChanged: widget.onDateFormatChanged!, itemCount: widget.options!.length,
onTimeFormatChanged: widget.onTimeFormatChanged!, separatorBuilder: (_, __) => const _GroupSeparator(),
itemBuilder: (_, index) =>
_renderGroupOptions(widget.options![index].options),
), ),
if (widget.onClearDate != null) ...[
const VSpace(4.0),
ClearDateButton(onClearDate: widget.onClearDate!),
], ],
], ],
), ),
); );
} }
Widget _renderGroupOptions(List<Widget> options) => ListView.separated(
shrinkWrap: true,
itemCount: options.length,
separatorBuilder: (_, __) => const VSpace(4),
itemBuilder: (_, index) => options[index],
);
}
class _GroupSeparator extends StatelessWidget {
const _GroupSeparator();
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Container(color: Theme.of(context).dividerColor, height: 1.0),
);
} }

View File

@ -0,0 +1,468 @@
import 'package:flutter/cupertino.dart';
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/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.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:go_router/go_router.dart';
class MobileAppFlowyDatePicker extends StatefulWidget {
const MobileAppFlowyDatePicker({
super.key,
this.selectedDay,
this.startDay,
this.endDay,
this.dateStr,
this.endDateStr,
this.timeStr,
this.endTimeStr,
this.enableRanges = false,
this.isRange = false,
this.rebuildOnDaySelected = false,
this.rebuildOnTimeChanged = false,
required this.includeTime,
required this.use24hFormat,
this.selectedReminderOption,
required this.onStartTimeChanged,
this.onEndTimeChanged,
required this.onIncludeTimeChanged,
this.onRangeChanged,
this.onDaySelected,
this.onRangeSelected,
this.onClearDate,
this.liveDateFormatter,
this.onReminderSelected,
});
final DateTime? selectedDay;
final DateTime? startDay;
final DateTime? endDay;
final String? dateStr;
final String? endDateStr;
final String? timeStr;
final String? endTimeStr;
final bool enableRanges;
final bool isRange;
final bool includeTime;
final bool rebuildOnDaySelected;
final bool rebuildOnTimeChanged;
final bool use24hFormat;
final ReminderOption? selectedReminderOption;
final Function(String? time) onStartTimeChanged;
final Function(String? time)? onEndTimeChanged;
final Function(bool) onIncludeTimeChanged;
final Function(bool)? onRangeChanged;
final DaySelectedCallback? onDaySelected;
final RangeSelectedCallback? onRangeSelected;
final VoidCallback? onClearDate;
final OnReminderSelected? onReminderSelected;
final String Function(DateTime)? liveDateFormatter;
@override
State<MobileAppFlowyDatePicker> createState() =>
_MobileAppFlowyDatePickerState();
}
class _MobileAppFlowyDatePickerState extends State<MobileAppFlowyDatePicker> {
late bool _includeTime = widget.includeTime;
late String? _dateStr = widget.dateStr;
late ReminderOption _reminderOption =
widget.selectedReminderOption ?? ReminderOption.none;
@override
Widget build(BuildContext context) {
return Column(
children: [
FlowyOptionDecorateBox(
showTopBorder: false,
child: _IncludeTimePicker(
dateStr:
widget.liveDateFormatter != null ? _dateStr : widget.dateStr,
endDateStr: widget.endDateStr,
timeStr: widget.timeStr,
endTimeStr: widget.endTimeStr,
includeTime: _includeTime,
use24hFormat: widget.use24hFormat,
onStartTimeChanged: widget.onStartTimeChanged,
onEndTimeChanged: widget.onEndTimeChanged,
rebuildOnTimeChanged: widget.rebuildOnTimeChanged,
),
),
const _Divider(),
FlowyOptionDecorateBox(
child: MobileDatePicker(
isRange: widget.isRange,
selectedDay: widget.selectedDay,
startDay: widget.startDay,
endDay: widget.endDay,
onDaySelected: (selected, focused) {
widget.onDaySelected?.call(selected, focused);
if (widget.liveDateFormatter != null) {
setState(() => _dateStr = widget.liveDateFormatter!(selected));
}
},
onRangeSelected: widget.onRangeSelected,
rebuildOnDaySelected: widget.rebuildOnDaySelected,
),
),
const _Divider(),
if (widget.enableRanges && widget.onRangeChanged != null)
_EndDateSwitch(
isRange: widget.isRange,
onRangeChanged: widget.onRangeChanged!,
),
_IncludeTimeSwitch(
showTopBorder: !widget.enableRanges || widget.onRangeChanged == null,
includeTime: _includeTime,
onIncludeTimeChanged: (includeTime) {
widget.onIncludeTimeChanged(includeTime);
setState(() => _includeTime = includeTime);
},
),
if (widget.onReminderSelected != null) ...[
const _Divider(),
_ReminderSelector(
selectedReminderOption: _reminderOption,
onReminderSelected: (option) {
widget.onReminderSelected!.call(option);
setState(() => _reminderOption = option);
},
),
],
if (widget.onClearDate != null) ...[
const _Divider(),
_ClearDateButton(onClearDate: widget.onClearDate!),
],
const _Divider(),
],
);
}
}
class _Divider extends StatelessWidget {
const _Divider();
@override
Widget build(BuildContext context) => const VSpace(20.0);
}
class _ReminderSelector extends StatelessWidget {
const _ReminderSelector({
this.selectedReminderOption,
required this.onReminderSelected,
});
final ReminderOption? selectedReminderOption;
final OnReminderSelected onReminderSelected;
@override
Widget build(BuildContext context) {
final option = selectedReminderOption ?? ReminderOption.none;
final availableOptions = [...ReminderOption.values];
if (option != ReminderOption.custom) {
availableOptions.remove(ReminderOption.custom);
}
return FlowyOptionTile.text(
text: 'Reminder',
trailing: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const HSpace(6.0),
FlowyText(
option.label,
color: Theme.of(context).hintColor,
),
const HSpace(4.0),
FlowySvg(
FlowySvgs.arrow_right_s,
color: Theme.of(context).hintColor,
size: const Size.square(18.0),
),
],
),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: 0.7,
minChildSize: 0.7,
builder: (context, controller) => Column(
children: [
const _ReminderSelectHeader(),
const VSpace(12.0),
Flexible(
child: SingleChildScrollView(
controller: controller,
child: Column(
children: availableOptions
.map(
(o) => FlowyOptionTile.text(
text: o.label,
showTopBorder: o == ReminderOption.none,
onTap: () {
onReminderSelected(o);
context.pop();
},
),
)
.toList(),
),
),
),
],
),
);
},
),
);
}
}
class _ReminderSelectHeader extends StatelessWidget {
const _ReminderSelectHeader();
@override
Widget build(BuildContext context) {
return SizedBox(
height: 56,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 120,
child: AppBarCancelButton(onTap: context.pop),
),
const FlowyText.medium(
'Select reminder',
fontSize: 17.0,
),
const HSpace(120),
],
),
);
}
}
class _IncludeTimePicker extends StatefulWidget {
const _IncludeTimePicker({
required this.includeTime,
this.dateStr,
this.endDateStr,
this.timeStr,
this.endTimeStr,
this.rebuildOnTimeChanged = false,
required this.use24hFormat,
required this.onStartTimeChanged,
required this.onEndTimeChanged,
});
final bool includeTime;
final String? dateStr;
final String? endDateStr;
final String? timeStr;
final String? endTimeStr;
final bool rebuildOnTimeChanged;
final bool use24hFormat;
final Function(String? time) onStartTimeChanged;
final Function(String? time)? onEndTimeChanged;
@override
State<_IncludeTimePicker> createState() => _IncludeTimePickerState();
}
class _IncludeTimePickerState extends State<_IncludeTimePicker> {
late String? _timeStr = widget.timeStr;
late String? _endTimeStr = widget.endTimeStr;
@override
Widget build(BuildContext context) {
if (widget.dateStr == null || widget.dateStr!.isEmpty) {
return const Divider(height: 1);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTime(
context,
widget.includeTime,
widget.use24hFormat,
true,
widget.dateStr,
widget.rebuildOnTimeChanged ? _timeStr : widget.timeStr,
),
VSpace(8.0, color: Theme.of(context).colorScheme.surface),
_buildTime(
context,
widget.includeTime,
widget.use24hFormat,
false,
widget.endDateStr,
widget.rebuildOnTimeChanged ? _endTimeStr : widget.endTimeStr,
),
],
),
);
}
Widget _buildTime(
BuildContext context,
bool isIncludeTime,
bool use24hFormat,
bool isStartDay,
String? dateStr,
String? timeStr,
) {
if (dateStr == null) {
return const SizedBox.shrink();
}
final List<Widget> children = [];
if (!isIncludeTime) {
children.addAll([
const HSpace(12.0),
FlowyText(dateStr),
]);
} else {
children.addAll([
Expanded(child: FlowyText(dateStr, textAlign: TextAlign.center)),
Container(width: 1, height: 16, color: Colors.grey),
Expanded(child: FlowyText(timeStr ?? '', textAlign: TextAlign.center)),
]);
}
return GestureDetector(
onTap: !isIncludeTime
? null
: () async {
await showMobileBottomSheet(
context,
builder: (context) => ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: CupertinoDatePicker(
showDayOfWeek: false,
mode: CupertinoDatePickerMode.time,
use24hFormat: use24hFormat,
onDateTimeChanged: (dateTime) {
final selectedTime = use24hFormat
? DateFormat('HH:mm').format(dateTime)
: DateFormat('hh:mm a').format(dateTime);
if (isStartDay) {
widget.onStartTimeChanged(selectedTime);
if (widget.rebuildOnTimeChanged && mounted) {
setState(() => _timeStr = selectedTime);
}
} else {
widget.onEndTimeChanged?.call(selectedTime);
if (widget.rebuildOnTimeChanged && mounted) {
setState(() => _endTimeStr = selectedTime);
}
}
},
),
),
);
},
child: Container(
constraints: const BoxConstraints(minHeight: 36),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Theme.of(context).colorScheme.secondaryContainer,
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
child: Row(children: children),
),
);
}
}
class _EndDateSwitch extends StatelessWidget {
const _EndDateSwitch({
required this.isRange,
required this.onRangeChanged,
});
final bool isRange;
final Function(bool) onRangeChanged;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.toggle(
text: LocaleKeys.grid_field_isRange.tr(),
isSelected: isRange,
onValueChanged: onRangeChanged,
);
}
}
class _IncludeTimeSwitch extends StatelessWidget {
const _IncludeTimeSwitch({
this.showTopBorder = true,
required this.includeTime,
required this.onIncludeTimeChanged,
});
final bool showTopBorder;
final bool includeTime;
final Function(bool) onIncludeTimeChanged;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.toggle(
showTopBorder: showTopBorder,
text: LocaleKeys.grid_field_includeTime.tr(),
isSelected: includeTime,
onValueChanged: onIncludeTimeChanged,
);
}
}
class _ClearDateButton extends StatelessWidget {
const _ClearDateButton({required this.onClearDate});
final VoidCallback onClearDate;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.text(
text: LocaleKeys.grid_field_clearDate.tr(),
onTap: onClearDate,
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';
@ -20,6 +21,8 @@ class DatePicker extends StatefulWidget {
this.lastDay, this.lastDay,
this.onDaySelected, this.onDaySelected,
this.onRangeSelected, this.onRangeSelected,
this.onCalendarCreated,
this.onPageChanged,
}); });
final bool isRange; final bool isRange;
@ -48,12 +51,16 @@ class DatePicker extends StatefulWidget {
DateTime focusedDay, DateTime focusedDay,
)? onRangeSelected; )? onRangeSelected;
final void Function(PageController pageController)? onCalendarCreated;
final void Function(DateTime focusedDay)? onPageChanged;
@override @override
State<DatePicker> createState() => _DatePickerState(); State<DatePicker> createState() => _DatePickerState();
} }
class _DatePickerState extends State<DatePicker> { class _DatePickerState extends State<DatePicker> {
DateTime _focusedDay = DateTime.now(); late DateTime _focusedDay = widget.selectedDay ?? DateTime.now();
late CalendarFormat _calendarFormat = widget.calendarFormat; late CalendarFormat _calendarFormat = widget.calendarFormat;
@override @override
@ -64,57 +71,56 @@ class _DatePickerState extends State<DatePicker> {
shape: BoxShape.circle, shape: BoxShape.circle,
); );
final calendarStyle = PlatformExtension.isMobile
? _CalendarStyle.mobile(
dowTextStyle: textStyle.copyWith(
color: Theme.of(context).hintColor,
fontSize: 14.0,
),
)
: _CalendarStyle.desktop(
textStyle: textStyle,
iconColor: Theme.of(context).iconTheme.color,
dowTextStyle: AFThemeExtension.of(context).caption,
selectedColor: Theme.of(context).colorScheme.primary,
);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TableCalendar( child: TableCalendar(
firstDay: widget.firstDay ?? kFirstDay, firstDay: widget.firstDay ?? kFirstDay,
lastDay: widget.lastDay ?? kLastDay, lastDay: widget.lastDay ?? kLastDay,
focusedDay: _focusedDay, focusedDay: _focusedDay,
rowHeight: 26.0 + 7.0, rowHeight: calendarStyle.rowHeight,
calendarFormat: _calendarFormat, calendarFormat: _calendarFormat,
availableCalendarFormats: const {CalendarFormat.month: 'Month'}, daysOfWeekHeight: calendarStyle.dowHeight,
daysOfWeekHeight: 17.0 + 8.0,
rangeSelectionMode: widget.isRange rangeSelectionMode: widget.isRange
? RangeSelectionMode.enforced ? RangeSelectionMode.enforced
: RangeSelectionMode.disabled, : RangeSelectionMode.disabled,
rangeStartDay: widget.isRange ? widget.startDay : null, rangeStartDay: widget.isRange ? widget.startDay : null,
rangeEndDay: widget.isRange ? widget.endDay : null, rangeEndDay: widget.isRange ? widget.endDay : null,
headerStyle: HeaderStyle( availableGestures: calendarStyle.availableGestures,
formatButtonVisible: false, availableCalendarFormats: const {CalendarFormat.month: 'Month'},
titleCentered: true, onCalendarCreated: widget.onCalendarCreated,
titleTextStyle: textStyle, headerVisible: calendarStyle.headerVisible,
leftChevronMargin: EdgeInsets.zero, headerStyle: calendarStyle.headerStyle,
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( calendarStyle: CalendarStyle(
cellMargin: const EdgeInsets.all(3.5), cellMargin: const EdgeInsets.all(3.5),
defaultDecoration: boxDecoration, defaultDecoration: boxDecoration,
selectedDecoration: boxDecoration.copyWith( selectedDecoration: boxDecoration.copyWith(
color: Theme.of(context).colorScheme.primary, color: calendarStyle.selectedColor,
), ),
todayDecoration: boxDecoration.copyWith( todayDecoration: boxDecoration.copyWith(
color: Colors.transparent, color: Colors.transparent,
border: Border.all(color: Theme.of(context).colorScheme.primary), border: Border.all(color: calendarStyle.selectedColor),
), ),
weekendDecoration: boxDecoration, weekendDecoration: boxDecoration,
outsideDecoration: boxDecoration, outsideDecoration: boxDecoration,
rangeStartDecoration: boxDecoration.copyWith( rangeStartDecoration: boxDecoration.copyWith(
color: Theme.of(context).colorScheme.primary, color: calendarStyle.selectedColor,
), ),
rangeEndDecoration: boxDecoration.copyWith( rangeEndDecoration: boxDecoration.copyWith(
color: Theme.of(context).colorScheme.primary, color: calendarStyle.selectedColor,
), ),
defaultTextStyle: textStyle, defaultTextStyle: textStyle,
weekendTextStyle: textStyle, weekendTextStyle: textStyle,
@ -140,10 +146,7 @@ class _DatePickerState extends State<DatePicker> {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: Center( child: Center(
child: Text( child: Text(label, style: calendarStyle.dowTextStyle),
label,
style: AFThemeExtension.of(context).caption,
),
), ),
); );
}, },
@ -152,10 +155,71 @@ class _DatePickerState extends State<DatePicker> {
widget.isRange ? false : isSameDay(widget.selectedDay, day), widget.isRange ? false : isSameDay(widget.selectedDay, day),
onFormatChanged: (calendarFormat) => onFormatChanged: (calendarFormat) =>
setState(() => _calendarFormat = calendarFormat), setState(() => _calendarFormat = calendarFormat),
onPageChanged: (focusedDay) => setState(() => _focusedDay = focusedDay), onPageChanged: (focusedDay) {
widget.onPageChanged?.call(focusedDay);
setState(() => _focusedDay = focusedDay);
},
onDaySelected: widget.onDaySelected, onDaySelected: widget.onDaySelected,
onRangeSelected: widget.onRangeSelected, onRangeSelected: widget.onRangeSelected,
), ),
); );
} }
} }
class _CalendarStyle {
_CalendarStyle({
required this.rowHeight,
required this.dowHeight,
required this.headerVisible,
required this.headerStyle,
required this.dowTextStyle,
required this.selectedColor,
required this.availableGestures,
});
final double rowHeight;
final double dowHeight;
final bool headerVisible;
final HeaderStyle headerStyle;
final TextStyle dowTextStyle;
final Color selectedColor;
final AvailableGestures availableGestures;
_CalendarStyle.mobile({
required this.dowTextStyle,
}) : rowHeight = 48,
dowHeight = 48,
headerVisible = false,
headerStyle = const HeaderStyle(),
selectedColor = const Color(0xFF00BCF0),
availableGestures = AvailableGestures.horizontalSwipe;
_CalendarStyle.desktop({
required TextStyle textStyle,
required this.selectedColor,
required this.dowTextStyle,
Color? iconColor,
}) : rowHeight = 33,
dowHeight = 35,
headerVisible = true,
headerStyle = HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: textStyle,
leftChevronMargin: EdgeInsets.zero,
leftChevronPadding: EdgeInsets.zero,
leftChevronIcon: FlowySvg(
FlowySvgs.arrow_left_s,
color: iconColor,
),
rightChevronPadding: EdgeInsets.zero,
rightChevronMargin: EdgeInsets.zero,
rightChevronIcon: FlowySvg(
FlowySvgs.arrow_right_s,
color: iconColor,
),
headerMargin: EdgeInsets.zero,
headerPadding: const EdgeInsets.only(bottom: 8.0),
),
availableGestures = AvailableGestures.all;
}

View File

@ -1,13 +1,13 @@
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; 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/date_time_format_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_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/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart';
/// Provides arguemnts for [AppFlowyDatePicker] when showing /// Provides arguemnts for [AppFlowyDatePicker] when showing
@ -21,15 +21,20 @@ class DatePickerOptions {
this.firstDay, this.firstDay,
this.lastDay, this.lastDay,
this.timeStr, this.timeStr,
this.endTimeStr,
this.includeTime = false, this.includeTime = false,
this.isRange = false, this.isRange = false,
this.enableRanges = true, this.enableRanges = true,
this.dateFormat = UserDateFormatPB.Friendly, this.dateFormat = UserDateFormatPB.Friendly,
this.timeFormat = UserTimeFormatPB.TwentyFourHour, this.timeFormat = UserTimeFormatPB.TwentyFourHour,
this.selectedReminderOption,
this.onDaySelected, this.onDaySelected,
this.onIncludeTimeChanged, required this.onIncludeTimeChanged,
this.onStartTimeChanged, this.onStartTimeChanged,
this.onEndTimeChanged, this.onEndTimeChanged,
this.onRangeSelected,
this.onIsRangeChanged,
this.onReminderSelected,
}) : focusedDay = focusedDay ?? DateTime.now(); }) : focusedDay = focusedDay ?? DateTime.now();
final DateTime focusedDay; final DateTime focusedDay;
@ -38,33 +43,35 @@ class DatePickerOptions {
final DateTime? firstDay; final DateTime? firstDay;
final DateTime? lastDay; final DateTime? lastDay;
final String? timeStr; final String? timeStr;
final String? endTimeStr;
final bool includeTime; final bool includeTime;
final bool isRange; final bool isRange;
final bool enableRanges; final bool enableRanges;
final UserDateFormatPB dateFormat; final UserDateFormatPB dateFormat;
final UserTimeFormatPB timeFormat; final UserTimeFormatPB timeFormat;
final ReminderOption? selectedReminderOption;
final DaySelectedCallback? onDaySelected; final DaySelectedCallback? onDaySelected;
final IncludeTimeChangedCallback? onIncludeTimeChanged; final IncludeTimeChangedCallback onIncludeTimeChanged;
final TimeChangedCallback? onStartTimeChanged; final TimeChangedCallback? onStartTimeChanged;
final TimeChangedCallback? onEndTimeChanged; final TimeChangedCallback? onEndTimeChanged;
final RangeSelectedCallback? onRangeSelected;
final Function(bool)? onIsRangeChanged;
final OnReminderSelected? onReminderSelected;
} }
abstract class DatePickerService { abstract class DatePickerService {
void show(Offset offset); void show(Offset offset, {required DatePickerOptions options});
void dismiss(); void dismiss();
} }
const double _datePickerWidth = 260; const double _datePickerWidth = 260;
const double _datePickerHeight = 355; const double _datePickerHeight = 370;
const double _includeTimeHeight = 40; const double _includeTimeHeight = 32;
const double _ySpacing = 15; const double _ySpacing = 15;
class DatePickerMenu extends DatePickerService { class DatePickerMenu extends DatePickerService {
DatePickerMenu({ DatePickerMenu({required this.context, required this.editorState});
required this.context,
required this.editorState,
});
final BuildContext context; final BuildContext context;
final EditorState editorState; final EditorState editorState;
@ -78,16 +85,10 @@ class DatePickerMenu extends DatePickerService {
} }
@override @override
void show( void show(Offset offset, {required DatePickerOptions options}) =>
Offset offset, {
DatePickerOptions? options,
}) =>
_show(offset, options: options); _show(offset, options: options);
void _show( void _show(Offset offset, {required DatePickerOptions options}) {
Offset offset, {
DatePickerOptions? options,
}) {
dismiss(); dismiss();
final editorSize = editorState.renderBox!.size; final editorSize = editorState.renderBox!.size;
@ -112,8 +113,7 @@ class DatePickerMenu extends DatePickerService {
} }
_menuEntry = OverlayEntry( _menuEntry = OverlayEntry(
builder: (context) { builder: (_) => Material(
return Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: SizedBox( child: SizedBox(
height: editorSize.height, height: editorSize.height,
@ -141,8 +141,7 @@ class DatePickerMenu extends DatePickerService {
), ),
), ),
), ),
); ),
},
); );
Overlay.of(context).insert(_menuEntry!); Overlay.of(context).insert(_menuEntry!);
@ -153,28 +152,28 @@ class _AnimatedDatePicker extends StatefulWidget {
const _AnimatedDatePicker({ const _AnimatedDatePicker({
required this.offset, required this.offset,
required this.showBelow, required this.showBelow,
this.options, required this.options,
}); });
final Offset offset; final Offset offset;
final bool showBelow; final bool showBelow;
final DatePickerOptions? options; final DatePickerOptions options;
@override @override
State<_AnimatedDatePicker> createState() => _AnimatedDatePickerState(); State<_AnimatedDatePicker> createState() => _AnimatedDatePickerState();
} }
class _AnimatedDatePickerState extends State<_AnimatedDatePicker> { class _AnimatedDatePickerState extends State<_AnimatedDatePicker> {
late bool _includeTime = widget.options?.includeTime ?? false; late bool _includeTime = widget.options.includeTime;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double dy = widget.offset.dy; double dy = widget.offset.dy;
if (!widget.showBelow && _includeTime) { if (!widget.showBelow && _includeTime) {
dy = dy - _includeTimeHeight; dy -= _includeTimeHeight;
} }
dy = dy + (widget.showBelow ? _ySpacing : -_ySpacing); dy += (widget.showBelow ? _ySpacing : -_ySpacing);
return AnimatedPositioned( return AnimatedPositioned(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
@ -185,30 +184,31 @@ class _AnimatedDatePickerState extends State<_AnimatedDatePicker> {
Theme.of(context).cardColor, Theme.of(context).cardColor,
Theme.of(context).colorScheme.shadow, Theme.of(context).colorScheme.shadow,
), ),
constraints: BoxConstraints.loose( constraints: BoxConstraints.loose(const Size(_datePickerWidth, 465)),
const Size(_datePickerWidth, 465),
),
child: AppFlowyDatePicker( child: AppFlowyDatePicker(
popoverMutex: widget.options?.popoverMutex,
includeTime: _includeTime, 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) { onIncludeTimeChanged: (includeTime) {
widget.options?.onIncludeTimeChanged?.call(!includeTime); widget.options.onIncludeTimeChanged.call(!includeTime);
setState(() => _includeTime = !includeTime); setState(() => _includeTime = !includeTime);
}, },
onStartTimeSubmitted: widget.options?.onStartTimeChanged, enableRanges: widget.options.enableRanges,
onDaySelected: widget.options?.onDaySelected, isRange: widget.options.isRange,
focusedDay: widget.options?.focusedDay ?? DateTime.now(), onIsRangeChanged: widget.options.onIsRangeChanged,
firstDay: widget.options?.firstDay, dateFormat: widget.options.dateFormat.simplified,
lastDay: widget.options?.lastDay, timeFormat: widget.options.timeFormat.simplified,
selectedDay: widget.options.selectedDay,
focusedDay: widget.options.focusedDay,
firstDay: widget.options.firstDay,
lastDay: widget.options.lastDay,
timeStr: widget.options.timeStr,
endTimeStr: widget.options.endTimeStr,
popoverMutex: widget.options.popoverMutex,
selectedReminderOption:
widget.options.selectedReminderOption ?? ReminderOption.none,
onStartTimeSubmitted: widget.options.onStartTimeChanged,
onDaySelected: widget.options.onDaySelected,
onRangeSelected: widget.options.onRangeSelected,
onReminderSelected: widget.options.onReminderSelected,
), ),
), ),
); );

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
import '../utils/layout.dart';
class DateTimeSetting extends StatefulWidget { class DateTimeSetting extends StatefulWidget {
const DateTimeSetting({ const DateTimeSetting({
@ -35,7 +36,7 @@ class _DateTimeSettingState extends State<DateTimeSetting> {
mutex: timeSettingPopoverMutex, mutex: timeSettingPopoverMutex,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0), offset: const Offset(8, 0),
popupBuilder: (BuildContext context) => DateFormatList( popupBuilder: (_) => DateFormatList(
selectedFormat: widget.dateFormat, selectedFormat: widget.dateFormat,
onSelected: _onDateFormatChanged, onSelected: _onDateFormatChanged,
), ),
@ -48,7 +49,7 @@ class _DateTimeSettingState extends State<DateTimeSetting> {
mutex: timeSettingPopoverMutex, mutex: timeSettingPopoverMutex,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0), offset: const Offset(8, 0),
popupBuilder: (BuildContext context) => TimeFormatList( popupBuilder: (_) => TimeFormatList(
selectedFormat: widget.timeFormat, selectedFormat: widget.timeFormat,
onSelected: _onTimeFormatChanged, onSelected: _onTimeFormatChanged,
), ),

View File

@ -32,7 +32,7 @@ class EndTextField extends StatelessWidget {
child: TimeTextField( child: TimeTextField(
isEndTime: true, isEndTime: true,
timeFormat: timeFormat, timeFormat: timeFormat,
timeStr: endTimeStr, endTimeStr: endTimeStr,
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
onSubmitted: onSubmitted, onSubmitted: onSubmitted,
), ),

View File

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class MobileDatePicker extends StatefulWidget {
const MobileDatePicker({
super.key,
this.selectedDay,
required this.isRange,
this.onDaySelected,
this.rebuildOnDaySelected = false,
this.onRangeSelected,
this.firstDay,
this.lastDay,
this.startDay,
this.endDay,
});
final DateTime? selectedDay;
final bool isRange;
final DaySelectedCallback? onDaySelected;
final bool rebuildOnDaySelected;
final RangeSelectedCallback? onRangeSelected;
final DateTime? firstDay;
final DateTime? lastDay;
final DateTime? startDay;
final DateTime? endDay;
@override
State<MobileDatePicker> createState() => _MobileDatePickerState();
}
class _MobileDatePickerState extends State<MobileDatePicker> {
PageController? _pageController;
late DateTime _focusedDay = widget.selectedDay ?? DateTime.now();
late DateTime? _selectedDay = widget.selectedDay;
@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) {
return DatePicker(
isRange: widget.isRange,
onDaySelected: (selectedDay, focusedDay) {
widget.onDaySelected?.call(selectedDay, focusedDay);
if (widget.rebuildOnDaySelected) {
setState(() => _selectedDay = selectedDay);
}
},
onRangeSelected: widget.onRangeSelected,
selectedDay:
widget.rebuildOnDaySelected ? _selectedDay : widget.selectedDay,
firstDay: widget.firstDay,
lastDay: widget.lastDay,
startDay: widget.startDay,
endDay: widget.endDay,
onCalendarCreated: (pageController) => _pageController = pageController,
onPageChanged: (focusedDay) => setState(() => _focusedDay = focusedDay),
);
}
Widget _buildHeader(BuildContext context) {
return Row(
children: [
const HSpace(16.0),
FlowyText(
DateFormat.yMMMM().format(_focusedDay),
),
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),
],
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
const _iconWidth = 30.0;
const _height = 44.0;
class MobileDateHeader extends StatelessWidget {
const MobileDateHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(_height),
),
width: _iconWidth,
iconPadding: EdgeInsets.zero,
onPressed: () => context.pop(),
),
),
Align(
alignment: Alignment.center,
child: FlowyText.medium(
LocaleKeys.grid_field_dateFieldName.tr(),
fontSize: 16,
),
),
].map((e) => SizedBox(height: _height, child: e)).toList(),
),
);
}
}

View File

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
typedef OnReminderSelected = void Function(ReminderOption option);
class ReminderSelector extends StatelessWidget {
const ReminderSelector({
super.key,
required this.mutex,
required this.selectedOption,
required this.onOptionSelected,
});
final PopoverMutex? mutex;
final ReminderOption selectedOption;
final OnReminderSelected? onOptionSelected;
@override
Widget build(BuildContext context) {
final options = ReminderOption.values.toList();
if (selectedOption != ReminderOption.custom) {
options.remove(ReminderOption.custom);
}
final optionWidgets = options
.map(
(o) => SizedBox(
height: DatePickerSize.itemHeight,
child: FlowyButton(
text: FlowyText.medium(o.label),
rightIcon: o == selectedOption
? const FlowySvg(FlowySvgs.check_s)
: null,
onTap: () {
if (o != selectedOption) {
onOptionSelected?.call(o);
mutex?.close();
}
},
),
),
)
.toList();
return AppFlowyPopover(
mutex: mutex,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, -155),
margin: EdgeInsets.zero,
constraints: BoxConstraints.loose(const Size(150, 310)),
popupBuilder: (_) => Padding(
padding: const EdgeInsets.all(6.0),
child: ListView.separated(
itemCount: options.length,
separatorBuilder: (_, __) => VSpace(DatePickerSize.seperatorHeight),
itemBuilder: (_, index) => optionWidgets[index],
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SizedBox(
height: DatePickerSize.itemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.datePicker_reminderLabel.tr()),
rightIcon: Row(
children: [
FlowyText.regular(selectedOption.label),
const FlowySvg(FlowySvgs.more_s),
],
),
),
),
),
);
}
}
enum ReminderOption {
none(time: Duration()),
atTimeOfEvent(time: Duration()),
fiveMinsBefore(time: Duration(minutes: 5)),
tenMinsBefore(time: Duration(minutes: 10)),
fifteenMinsBefore(time: Duration(minutes: 15)),
thirtyMinsBefore(time: Duration(minutes: 30)),
oneHourBefore(time: Duration(hours: 1)),
twoHoursBefore(time: Duration(hours: 2)),
oneDayBefore(time: Duration(days: 1)),
twoDaysBefore(time: Duration(days: 2)),
custom(time: Duration());
const ReminderOption({required this.time});
final Duration time;
String get label => switch (this) {
ReminderOption.none => LocaleKeys.datePicker_reminderOptions_none.tr(),
ReminderOption.atTimeOfEvent =>
LocaleKeys.datePicker_reminderOptions_atTimeOfEvent.tr(),
ReminderOption.fiveMinsBefore =>
LocaleKeys.datePicker_reminderOptions_fiveMinsBefore.tr(),
ReminderOption.tenMinsBefore =>
LocaleKeys.datePicker_reminderOptions_tenMinsBefore.tr(),
ReminderOption.fifteenMinsBefore =>
LocaleKeys.datePicker_reminderOptions_fifteenMinsBefore.tr(),
ReminderOption.thirtyMinsBefore =>
LocaleKeys.datePicker_reminderOptions_thirtyMinsBefore.tr(),
ReminderOption.oneHourBefore =>
LocaleKeys.datePicker_reminderOptions_oneHourBefore.tr(),
ReminderOption.twoHoursBefore =>
LocaleKeys.datePicker_reminderOptions_twoHoursBefore.tr(),
ReminderOption.oneDayBefore =>
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
ReminderOption.twoDaysBefore =>
LocaleKeys.datePicker_reminderOptions_twoDaysBefore.tr(),
ReminderOption.custom =>
LocaleKeys.datePicker_reminderOptions_custom.tr(),
};
static ReminderOption fromDateDifference(
DateTime eventDate,
DateTime reminderDate,
) =>
fromMinutes(eventDate.difference(reminderDate).inMinutes);
static ReminderOption fromMinutes(int minutes) => switch (minutes) {
0 => ReminderOption.atTimeOfEvent,
5 => ReminderOption.fiveMinsBefore,
10 => ReminderOption.tenMinsBefore,
15 => ReminderOption.fifteenMinsBefore,
30 => ReminderOption.thirtyMinsBefore,
60 => ReminderOption.oneHourBefore,
120 => ReminderOption.twoHoursBefore,
1440 => ReminderOption.oneDayBefore,
2880 => ReminderOption.twoDaysBefore,
_ => ReminderOption.custom,
};
}

View File

@ -45,11 +45,8 @@ class _TimeTextFieldState extends State<TimeTextField> {
void initState() { void initState() {
super.initState(); super.initState();
if (widget.isEndTime) { _textController.text =
_textController.text = widget.endTimeStr ?? ""; (widget.isEndTime ? widget.endTimeStr : widget.timeStr) ?? "";
} else {
_textController.text = widget.timeStr ?? "";
}
if (!widget.isEndTime && widget.timeStr != null) { if (!widget.isEndTime && widget.timeStr != null) {
text = widget.timeStr!; text = widget.timeStr!;
@ -89,6 +86,7 @@ class _TimeTextFieldState extends State<TimeTextField> {
child: FlowyTextField( child: FlowyTextField(
text: text, text: text,
focusNode: _focusNode, focusNode: _focusNode,
autoFocus: false,
controller: _textController, controller: _textController,
submitOnLeave: true, submitOnLeave: true,
hintText: widget.timeHintText, hintText: widget.timeHintText,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
class AppFlowyPopover extends StatelessWidget { class AppFlowyPopover extends StatelessWidget {
final Widget child; final Widget child;
@ -58,12 +59,11 @@ class AppFlowyPopover extends StatelessWidget {
offset: offset, offset: offset,
clickHandler: clickHandler, clickHandler: clickHandler,
popupBuilder: (context) { popupBuilder: (context) {
final child = popupBuilder(context);
return _PopoverContainer( return _PopoverContainer(
constraints: constraints, constraints: constraints,
margin: margin, margin: margin,
decoration: decoration, decoration: decoration,
child: child, child: popupBuilder(context),
); );
}, },
child: child, child: child,
@ -72,18 +72,17 @@ class AppFlowyPopover extends StatelessWidget {
} }
class _PopoverContainer extends StatelessWidget { class _PopoverContainer extends StatelessWidget {
final Widget child;
final BoxConstraints constraints;
final EdgeInsets margin;
final Decoration? decoration;
const _PopoverContainer({ const _PopoverContainer({
required this.child, required this.child,
required this.margin, required this.margin,
required this.constraints, required this.constraints,
required this.decoration, required this.decoration,
Key? key, });
}) : super(key: key);
final Widget child;
final BoxConstraints constraints;
final EdgeInsets margin;
final Decoration? decoration;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -581,7 +581,8 @@
"newProperty": "New property", "newProperty": "New property",
"deleteFieldPromptMessage": "Are you sure? This property will be deleted", "deleteFieldPromptMessage": "Are you sure? This property will be deleted",
"newColumn": "New Column", "newColumn": "New Column",
"format": "Format" "format": "Format",
"reminderOnDateTooltip": "This cell has a scheduled reminder"
}, },
"rowPage": { "rowPage": {
"newField": "Add a new field", "newField": "Add a new field",
@ -1025,7 +1026,21 @@
"includeTime": "Include time", "includeTime": "Include time",
"isRange": "End date", "isRange": "End date",
"timeFormat": "Time format", "timeFormat": "Time format",
"clearDate": "Clear date" "clearDate": "Clear date",
"reminderLabel": "Reminder",
"reminderOptions": {
"none": "None",
"atTimeOfEvent": "Time of event",
"fiveMinsBefore": "5 mins before",
"tenMinsBefore": "10 mins before",
"fifteenMinsBefore": "15 mins before",
"thirtyMinsBefore": "30 mins before",
"oneHourBefore": "1 hour before",
"twoHoursBefore": "2 hours before",
"oneDayBefore": "1 day before",
"twoDaysBefore": "2 days before",
"custom": "Custom"
}
}, },
"relativeDates": { "relativeDates": {
"yesterday": "Yesterday", "yesterday": "Yesterday",
@ -1089,6 +1104,7 @@
"highlight": "Highlight", "highlight": "Highlight",
"color": "Color", "color": "Color",
"image": "Image", "image": "Image",
"date": "Date",
"italic": "Italic", "italic": "Italic",
"link": "Link", "link": "Link",
"numberedList": "Numbered List", "numberedList": "Numbered List",

View File

@ -34,7 +34,7 @@ resolver = "2"
[workspace.dependencies] [workspace.dependencies]
lib-dispatch = { workspace = true, path = "lib-dispatch" } lib-dispatch = { workspace = true, path = "lib-dispatch" }
lib-log = { workspace = true, path = "lib-log" } lib-log = { workspace = true, path = "lib-log" }
lib-infra= { workspace = true, path = "lib-infra" } lib-infra = { workspace = true, path = "lib-infra" }
flowy-ast = { workspace = true, path = "build-tool/flowy-ast" } flowy-ast = { workspace = true, path = "build-tool/flowy-ast" }
flowy-codegen = { workspace = true, path = "build-tool/flowy-codegen" } flowy-codegen = { workspace = true, path = "build-tool/flowy-codegen" }
flowy-derive = { workspace = true, path = "build-tool/flowy-derive" } flowy-derive = { workspace = true, path = "build-tool/flowy-derive" }

View File

@ -32,6 +32,9 @@ pub struct DateCellDataPB {
#[pb(index = 8)] #[pb(index = 8)]
pub is_range: bool, pub is_range: bool,
#[pb(index = 9)]
pub reminder_id: String,
} }
#[derive(Clone, Debug, Default, ProtoBuf)] #[derive(Clone, Debug, Default, ProtoBuf)]
@ -59,6 +62,9 @@ pub struct DateChangesetPB {
#[pb(index = 8, one_of)] #[pb(index = 8, one_of)]
pub clear_flag: Option<bool>, pub clear_flag: Option<bool>,
#[pb(index = 9, one_of)]
pub reminder_id: Option<String>,
} }
// Date // Date
@ -94,7 +100,7 @@ impl From<DateTypeOptionPB> for DateTypeOption {
} }
} }
#[derive(Clone, Debug, Copy, EnumIter, ProtoBuf_Enum, Default)] #[derive(Clone, Debug, Copy, ProtoBuf_Enum, Default)]
pub enum DateFormatPB { pub enum DateFormatPB {
Local = 0, Local = 0,
US = 1, US = 1,

View File

@ -586,7 +586,9 @@ pub(crate) async fn update_date_cell_handler(
include_time: data.include_time, include_time: data.include_time,
is_range: data.is_range, is_range: data.is_range,
clear_flag: data.clear_flag, clear_flag: data.clear_flag,
reminder_id: data.reminder_id,
}; };
let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?; let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?;
database_editor database_editor
.update_cell_with_changeset( .update_cell_with_changeset(

View File

@ -79,6 +79,8 @@ impl TypeOptionCellDataSerde for DateTypeOption {
let end_timestamp = cell_data.end_timestamp; let end_timestamp = cell_data.end_timestamp;
let (end_date, end_time) = self.formatted_date_time_from_timestamp(&end_timestamp); let (end_date, end_time) = self.formatted_date_time_from_timestamp(&end_timestamp);
let reminder_id = cell_data.reminder_id;
DateCellDataPB { DateCellDataPB {
date, date,
time, time,
@ -88,6 +90,7 @@ impl TypeOptionCellDataSerde for DateTypeOption {
end_timestamp: end_timestamp.unwrap_or_default(), end_timestamp: end_timestamp.unwrap_or_default(),
include_time, include_time,
is_range, is_range,
reminder_id,
} }
} }
@ -257,7 +260,8 @@ impl CellDataChangeset for DateTypeOption {
cell: Option<Cell>, cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> { ) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
// old date cell data // old date cell data
let (previous_timestamp, previous_end_timestamp, include_time, is_range) = match cell { let (previous_timestamp, previous_end_timestamp, include_time, is_range, reminder_id) =
match cell {
Some(cell) => { Some(cell) => {
let cell_data = DateCellData::from(&cell); let cell_data = DateCellData::from(&cell);
( (
@ -265,9 +269,10 @@ impl CellDataChangeset for DateTypeOption {
cell_data.end_timestamp, cell_data.end_timestamp,
cell_data.include_time, cell_data.include_time,
cell_data.is_range, cell_data.is_range,
cell_data.reminder_id,
) )
}, },
None => (None, None, false, false), None => (None, None, false, false, String::new()),
}; };
if changeset.clear_flag == Some(true) { if changeset.clear_flag == Some(true) {
@ -276,6 +281,7 @@ impl CellDataChangeset for DateTypeOption {
end_timestamp: None, end_timestamp: None,
include_time, include_time,
is_range, is_range,
reminder_id: String::new(),
}; };
return Ok((Cell::from(&cell_data), cell_data)); return Ok((Cell::from(&cell_data), cell_data));
@ -284,6 +290,7 @@ impl CellDataChangeset for DateTypeOption {
// update include_time and is_range if necessary // update include_time and is_range if necessary
let include_time = changeset.include_time.unwrap_or(include_time); let include_time = changeset.include_time.unwrap_or(include_time);
let is_range = changeset.is_range.unwrap_or(is_range); let is_range = changeset.is_range.unwrap_or(is_range);
let reminder_id = changeset.reminder_id.unwrap_or(reminder_id);
// Calculate the timestamp in the time zone specified in type option. If // Calculate the timestamp in the time zone specified in type option. If
// a new timestamp is included in the changeset without an accompanying // a new timestamp is included in the changeset without an accompanying
@ -323,6 +330,7 @@ impl CellDataChangeset for DateTypeOption {
end_timestamp, end_timestamp,
include_time, include_time,
is_range, is_range,
reminder_id,
}; };
Ok((Cell::from(&cell_data), cell_data)) Ok((Cell::from(&cell_data), cell_data))

View File

@ -25,6 +25,7 @@ pub struct DateCellChangeset {
pub include_time: Option<bool>, pub include_time: Option<bool>,
pub is_range: Option<bool>, pub is_range: Option<bool>,
pub clear_flag: Option<bool>, pub clear_flag: Option<bool>,
pub reminder_id: Option<String>,
} }
impl FromCellChangeset for DateCellChangeset { impl FromCellChangeset for DateCellChangeset {
@ -50,15 +51,17 @@ pub struct DateCellData {
pub include_time: bool, pub include_time: bool,
#[serde(default)] #[serde(default)]
pub is_range: bool, pub is_range: bool,
pub reminder_id: String,
} }
impl DateCellData { impl DateCellData {
pub fn new(timestamp: i64, include_time: bool, is_range: bool) -> Self { pub fn new(timestamp: i64, include_time: bool, is_range: bool, reminder_id: String) -> Self {
Self { Self {
timestamp: Some(timestamp), timestamp: Some(timestamp),
end_timestamp: None, end_timestamp: None,
include_time, include_time,
is_range, is_range,
reminder_id,
} }
} }
} }
@ -79,11 +82,14 @@ impl From<&Cell> for DateCellData {
.and_then(|data| data.parse::<i64>().ok()); .and_then(|data| data.parse::<i64>().ok());
let include_time = cell.get_bool_value("include_time").unwrap_or_default(); let include_time = cell.get_bool_value("include_time").unwrap_or_default();
let is_range = cell.get_bool_value("is_range").unwrap_or_default(); let is_range = cell.get_bool_value("is_range").unwrap_or_default();
let reminder_id = cell.get_str_value("reminder_id").unwrap_or_default();
Self { Self {
timestamp, timestamp,
end_timestamp, end_timestamp,
include_time, include_time,
is_range, is_range,
reminder_id,
} }
} }
} }
@ -95,6 +101,7 @@ impl From<&DateCellDataPB> for DateCellData {
end_timestamp: Some(data.end_timestamp), end_timestamp: Some(data.end_timestamp),
include_time: data.include_time, include_time: data.include_time,
is_range: data.is_range, is_range: data.is_range,
reminder_id: data.reminder_id.to_owned(),
} }
} }
} }
@ -116,6 +123,7 @@ impl From<&DateCellData> for Cell {
.insert_str_value("end_timestamp", end_timestamp_string) .insert_str_value("end_timestamp", end_timestamp_string)
.insert_bool_value("include_time", cell_data.include_time) .insert_bool_value("include_time", cell_data.include_time)
.insert_bool_value("is_range", cell_data.is_range) .insert_bool_value("is_range", cell_data.is_range)
.insert_str_value("reminder_id", cell_data.reminder_id.to_owned())
.build() .build()
} }
} }
@ -145,6 +153,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
end_timestamp: None, end_timestamp: None,
include_time: false, include_time: false,
is_range: false, is_range: false,
reminder_id: String::new(),
}) })
} }
@ -163,6 +172,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
let mut end_timestamp: Option<i64> = None; let mut end_timestamp: Option<i64> = None;
let mut include_time: Option<bool> = None; let mut include_time: Option<bool> = None;
let mut is_range: Option<bool> = None; let mut is_range: Option<bool> = None;
let mut reminder_id: Option<String> = None;
while let Some(key) = map.next_key()? { while let Some(key) = map.next_key()? {
match key { match key {
@ -178,18 +188,23 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
"is_range" => { "is_range" => {
is_range = map.next_value()?; is_range = map.next_value()?;
}, },
"reminder_id" => {
reminder_id = map.next_value()?;
},
_ => {}, _ => {},
} }
} }
let include_time = include_time.unwrap_or_default(); let include_time = include_time.unwrap_or_default();
let is_range = is_range.unwrap_or_default(); let is_range = is_range.unwrap_or_default();
let reminder_id = reminder_id.unwrap_or_default();
Ok(DateCellData { Ok(DateCellData {
timestamp, timestamp,
end_timestamp, end_timestamp,
include_time, include_time,
is_range, is_range,
reminder_id,
}) })
} }
} }

View File

@ -29,6 +29,7 @@ mod tests {
end_timestamp: None, end_timestamp: None,
include_time: true, include_time: true,
is_range: false, is_range: false,
reminder_id: String::new(),
}; };
assert_eq!( assert_eq!(
@ -41,6 +42,7 @@ mod tests {
end_timestamp: Some(1648533809), end_timestamp: Some(1648533809),
include_time: true, include_time: true,
is_range: false, is_range: false,
reminder_id: String::new(),
}; };
assert_eq!( assert_eq!(
@ -53,6 +55,7 @@ mod tests {
end_timestamp: Some(1648533809), end_timestamp: Some(1648533809),
include_time: true, include_time: true,
is_range: true, is_range: true,
reminder_id: String::new(),
}; };
assert_eq!( assert_eq!(

View File

@ -115,6 +115,7 @@ pub fn make_date_cell_string(timestamp: i64) -> String {
include_time: Some(false), include_time: Some(false),
is_range: Some(false), is_range: Some(false),
clear_flag: None, clear_flag: None,
reminder_id: Some(String::new()),
}) })
.unwrap() .unwrap()
} }