mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: reminder on date (#4288)
* feat: support reminder on date * feat: support reminder on date in database * fix: include time static * fix: do not force unwrap * chore: clean flutter code * test: add test for reminder in database * fix: interpret reminder option * feat: date and reminder on mobile * feat: improve notification actions and support open row * feat: support dates in document * fix: minor changes + review * feat: support reminder on mobile in document * feat: support open row on database reminder mobile * test: add more tests * fix: first part of review * fix: open row responsibility * fix: abstract application logic from presentation layer * fix: update reminder on date cell update * test: fix failing test * fix: show correct selected day after end date toggled
This commit is contained in:
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
)..add(const DateCellEditorEvent.initial()),
|
||||||
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()),
|
|
||||||
child: const Column(
|
|
||||||
children: [
|
|
||||||
FlowyOptionDecorateBox(
|
|
||||||
showTopBorder: false,
|
|
||||||
child: _IncludeTimePicker(),
|
|
||||||
),
|
|
||||||
_Divider(),
|
|
||||||
FlowyOptionDecorateBox(
|
|
||||||
child: MobileDatePicker(),
|
|
||||||
),
|
|
||||||
_Divider(),
|
|
||||||
_EndDateSwitch(),
|
|
||||||
_IncludeTimeSwitch(),
|
|
||||||
_Divider(),
|
|
||||||
_ClearDateButton(),
|
|
||||||
_Divider(),
|
|
||||||
],
|
],
|
||||||
),
|
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
|
||||||
);
|
builder: (context, state) {
|
||||||
}
|
return MobileAppFlowyDatePicker(
|
||||||
}
|
selectedDay: state.dateTime,
|
||||||
|
dateStr: state.dateStr,
|
||||||
class _Divider extends StatelessWidget {
|
endDateStr: state.endDateStr,
|
||||||
const _Divider();
|
timeStr: state.timeStr,
|
||||||
|
endTimeStr: state.endTimeStr,
|
||||||
@override
|
startDay: state.startDay,
|
||||||
Widget build(BuildContext context) {
|
endDay: state.endDay,
|
||||||
return const VSpace(20.0);
|
enableRanges: true,
|
||||||
}
|
isRange: state.isRange,
|
||||||
}
|
includeTime: state.includeTime,
|
||||||
|
use24hFormat: state.dateTypeOptionPB.timeFormat ==
|
||||||
class _IncludeTimePicker extends StatefulWidget {
|
TimeFormatPB.TwentyFourHour,
|
||||||
const _IncludeTimePicker();
|
selectedReminderOption: state.reminderOption,
|
||||||
|
onStartTimeChanged: (String? time) {
|
||||||
@override
|
if (time != null) {
|
||||||
State<_IncludeTimePicker> createState() => _IncludeTimePickerState();
|
context
|
||||||
}
|
.read<DateCellEditorBloc>()
|
||||||
|
.add(DateCellEditorEvent.setTime(time));
|
||||||
class _IncludeTimePickerState extends State<_IncludeTimePicker> {
|
}
|
||||||
String? _selectedTime;
|
},
|
||||||
|
onEndTimeChanged: (String? time) {
|
||||||
@override
|
if (time != null) {
|
||||||
Widget build(BuildContext context) {
|
context
|
||||||
return BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
|
.read<DateCellEditorBloc>()
|
||||||
builder: (context, state) {
|
.add(DateCellEditorEvent.setEndTime(time));
|
||||||
final startDay = state.dateStr;
|
}
|
||||||
final endDay = state.endDateStr;
|
},
|
||||||
final includeTime = state.includeTime;
|
onDaySelected: (selectedDay, focusedDay) => context
|
||||||
final use24hFormat =
|
.read<DateCellEditorBloc>()
|
||||||
state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwentyFourHour;
|
.add(DateCellEditorEvent.selectDay(selectedDay)),
|
||||||
if (startDay == null || startDay.isEmpty) {
|
onRangeSelected: (start, end, focused) => context
|
||||||
return const Divider(
|
.read<DateCellEditorBloc>()
|
||||||
height: 1,
|
.add(DateCellEditorEvent.selectDateRange(start, end)),
|
||||||
);
|
onRangeChanged: (value) => context
|
||||||
}
|
.read<DateCellEditorBloc>()
|
||||||
return Container(
|
.add(DateCellEditorEvent.setIsRange(value)),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
onIncludeTimeChanged: (value) => context
|
||||||
child: Column(
|
.read<DateCellEditorBloc>()
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
.add(DateCellEditorEvent.setIncludeTime(value)),
|
||||||
children: [
|
onClearDate: () => context
|
||||||
_buildTime(
|
.read<DateCellEditorBloc>()
|
||||||
context,
|
.add(const DateCellEditorEvent.clearDate()),
|
||||||
includeTime,
|
onReminderSelected: (option) => context
|
||||||
use24hFormat,
|
.read<DateCellEditorBloc>()
|
||||||
true,
|
.add(DateCellEditorEvent.setReminderOption(option: option)),
|
||||||
startDay,
|
|
||||||
state.timeStr,
|
|
||||||
),
|
|
||||||
VSpace(
|
|
||||||
8.0,
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
),
|
|
||||||
_buildTime(
|
|
||||||
context,
|
|
||||||
includeTime,
|
|
||||||
use24hFormat,
|
|
||||||
false,
|
|
||||||
endDay,
|
|
||||||
state.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: () 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) {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _textController,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: state.timeHintText,
|
|
||||||
errorText: widget.isEndTime
|
|
||||||
? state.parseEndTimeError
|
|
||||||
: state.parseTimeError,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.datetime,
|
|
||||||
onFieldSubmitted: (timeStr) {
|
|
||||||
context.read<DateCellEditorBloc>().add(
|
|
||||||
widget.isEndTime
|
|
||||||
? DateCellEditorEvent.setEndTime(timeStr)
|
|
||||||
: DateCellEditorEvent.setTime(timeStr),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_textController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ClearDateButton extends StatelessWidget {
|
|
||||||
const _ClearDateButton();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FlowyOptionTile.text(
|
|
||||||
text: LocaleKeys.grid_field_clearDate.tr(),
|
|
||||||
onTap: () => context
|
|
||||||
.read<DateCellEditorBloc>()
|
|
||||||
.add(const DateCellEditorEvent.clearDate()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)),
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
(_) => GridShortcuts(child: GridPageContent(view: widget.view)),
|
_openRow(context, widget.initialRowId, true);
|
||||||
(err) => FlowyErrorPage.message(
|
return result.successOrFail.fold(
|
||||||
err.toString(),
|
(_) => GridShortcuts(child: GridPageContent(view: widget.view)),
|
||||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
(err) => FlowyErrorPage.message(
|
||||||
),
|
err.toString(),
|
||||||
),
|
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 {
|
||||||
|
@ -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>(
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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: FlowyText.medium(
|
child: Row(
|
||||||
text,
|
mainAxisSize: MainAxisSize.min,
|
||||||
color: color,
|
children: [
|
||||||
overflow: TextOverflow.ellipsis,
|
Flexible(
|
||||||
|
child: FlowyText.medium(
|
||||||
|
text,
|
||||||
|
color: color,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state.data?.reminderId.isNotEmpty == true) ...[
|
||||||
|
const HSpace(5),
|
||||||
|
FlowyTooltip(
|
||||||
|
message:
|
||||||
|
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
|
||||||
|
child: const FlowySvg(FlowySvgs.clock_alarm_s),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
popupBuilder: (BuildContext popoverContent) {
|
popupBuilder: (_) => DateCellEditor(
|
||||||
return DateCellEditor(
|
cellController: _cellController,
|
||||||
cellController: _cellController,
|
onDismissed: () => widget.cellContainerNotifier.isFocus = false,
|
||||||
onDismissed: () =>
|
),
|
||||||
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(
|
||||||
text,
|
children: [
|
||||||
color: color,
|
if (state.data?.reminderId.isNotEmpty == true) ...[
|
||||||
fontSize: 15,
|
FlowyTooltip(
|
||||||
maxLines: 1,
|
message:
|
||||||
|
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
|
||||||
|
child: const FlowySvg(FlowySvgs.clock_alarm_s),
|
||||||
|
),
|
||||||
|
const HSpace(5),
|
||||||
|
],
|
||||||
|
FlowyText(
|
||||||
|
text,
|
||||||
|
color: color,
|
||||||
|
fontSize: 15,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () => showMobileBottomSheet(
|
||||||
showMobileBottomSheet(
|
context,
|
||||||
context,
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
backgroundColor:
|
||||||
backgroundColor:
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
Theme.of(context).colorScheme.secondaryContainer,
|
builder: (_) => MobileDateCellEditScreen(
|
||||||
builder: (context) {
|
controller: _cellController,
|
||||||
return MobileDateCellEditScreen(
|
showAsFullScreen: false,
|
||||||
controller: _cellController,
|
),
|
||||||
showAsFullScreen: false,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5,15 +5,22 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
|
|||||||
import 'package:appflowy/plugins/database/application/cell/date_cell_service.dart';
|
import 'package:appflowy/plugins/database/application/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 {
|
|
||||||
await _updateDateData(includeTime: includeTime);
|
|
||||||
},
|
|
||||||
setIsRange: (isRange) async {
|
|
||||||
await _updateDateData(isRange: isRange);
|
|
||||||
},
|
},
|
||||||
|
setIncludeTime: (includeTime) async =>
|
||||||
|
await _updateDateData(includeTime: includeTime),
|
||||||
|
setIsRange: (isRange) async =>
|
||||||
|
await _updateDateData(isRange: isRange),
|
||||||
setTime: (timeStr) async {
|
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
create: (context) => DateCellEditorBloc(
|
providers: [
|
||||||
cellController: widget.cellController,
|
BlocProvider<DateCellEditorBloc>(
|
||||||
)..add(const DateCellEditorEvent.initial()),
|
create: (context) => DateCellEditorBloc(
|
||||||
|
reminderBloc: getIt<ReminderBloc>(),
|
||||||
|
cellController: widget.cellController,
|
||||||
|
)..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)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(
|
||||||
if (deletedView.hasIndex()) {
|
() => null,
|
||||||
deletedViewIndex = deletedView.index;
|
(deletedView) {
|
||||||
}
|
if (deletedView.hasIndex()) {
|
||||||
});
|
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(),
|
||||||
],
|
],
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,20 +423,19 @@ 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,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
|
||||||
child: FindAndReplaceMenuWidget(
|
|
||||||
editorState: editorState,
|
|
||||||
onDismiss: onDismiss,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
child: FindAndReplaceMenuWidget(
|
||||||
},
|
editorState: editorState,
|
||||||
|
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),
|
||||||
|
@ -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:
|
||||||
|
@ -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,
|
||||||
: null,
|
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,
|
||||||
|
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');
|
||||||
|
|
||||||
if (timeFormat == TimeFormatPB.TwelveHour) {
|
try {
|
||||||
return twelveHourFormat.parse(timeStr);
|
if (timeFormat == TimeFormatPB.TwelveHour) {
|
||||||
}
|
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;
|
||||||
ReminderEvent.update(
|
if (parsedDate == null || rootContext == null) {
|
||||||
ReminderUpdate(
|
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(
|
||||||
|
ReminderUpdate(
|
||||||
|
id: widget.reminderId!,
|
||||||
|
scheduledAt: parsedDate!.subtract(reminderOption.time),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final reminderId = nanoid();
|
||||||
|
_updateBlock(
|
||||||
|
parsedDate!,
|
||||||
|
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,
|
id: reminderId,
|
||||||
scheduledAt: selectedDay,
|
objectId: viewId,
|
||||||
includeTime: includeTime,
|
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()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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()),
|
||||||
|
@ -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!
|
|
||||||
.arguments?[ActionArgumentKeys.nodePath.name] as int?;
|
|
||||||
|
|
||||||
if (nodePath != null) {
|
|
||||||
context.read<NotificationActionBloc>().add(
|
|
||||||
NotificationActionEvent.performAction(
|
|
||||||
action: state.action!
|
|
||||||
.copyWith(type: ActionType.jumpToBlock),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
} else if (action?.type == ActionType.openRow &&
|
||||||
}
|
PlatformExtension.isMobile) {
|
||||||
|
final view = action!.arguments?[ActionArgumentKeys.view];
|
||||||
|
if (view != null) {
|
||||||
|
final view = action.arguments?[ActionArgumentKeys.view];
|
||||||
|
final rowId = action.arguments?[ActionArgumentKeys.rowId];
|
||||||
|
AppGlobals.rootNavKey.currentContext?.pushView(view, {
|
||||||
|
PluginArgumentKeys.rowId: rowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||||
builder: (context, state) => MaterialApp.router(
|
builder: (context, state) => MaterialApp.router(
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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];
|
||||||
objectId: reminder.objectId,
|
}
|
||||||
arguments: {
|
|
||||||
ActionArgumentKeys.nodePath.name: path,
|
final action = NotificationAction(
|
||||||
ActionArgumentKeys.view.name: view,
|
objectId: reminder.objectId,
|
||||||
},
|
arguments: {
|
||||||
),
|
ActionArgumentKeys.view: 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(
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
5
frontend/appflowy_flutter/lib/util/int64_extension.dart
Normal file
5
frontend/appflowy_flutter/lib/util/int64_extension.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
|
||||||
|
extension DateConversion on Int64 {
|
||||||
|
DateTime toDateTime() => DateTime.fromMillisecondsSinceEpoch(toInt() * 1000);
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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),
|
||||||
|
@ -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),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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,37 +113,35 @@ 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,
|
width: editorSize.width,
|
||||||
width: editorSize.width,
|
child: RawKeyboardListener(
|
||||||
child: RawKeyboardListener(
|
focusNode: FocusNode()..requestFocus(),
|
||||||
focusNode: FocusNode()..requestFocus(),
|
onKey: (event) {
|
||||||
onKey: (event) {
|
if (event is RawKeyDownEvent &&
|
||||||
if (event is RawKeyDownEvent &&
|
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
dismiss();
|
||||||
dismiss();
|
}
|
||||||
}
|
},
|
||||||
},
|
child: GestureDetector(
|
||||||
child: GestureDetector(
|
behavior: HitTestBehavior.opaque,
|
||||||
behavior: HitTestBehavior.opaque,
|
onTap: dismiss,
|
||||||
onTap: dismiss,
|
child: Stack(
|
||||||
child: Stack(
|
children: [
|
||||||
children: [
|
_AnimatedDatePicker(
|
||||||
_AnimatedDatePicker(
|
offset: Offset(offsetX, offsetY),
|
||||||
offset: Offset(offsetX, offsetY),
|
showBelow: showBelow,
|
||||||
showBelow: showBelow,
|
options: options,
|
||||||
options: options,
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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",
|
||||||
|
@ -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" }
|
||||||
@ -73,7 +73,7 @@ futures = "0.3.29"
|
|||||||
tokio = "1.34.0"
|
tokio = "1.34.0"
|
||||||
tokio-stream = "0.1.14"
|
tokio-stream = "0.1.14"
|
||||||
async-trait = "0.1.74"
|
async-trait = "0.1.74"
|
||||||
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
|
||||||
lru = "0.12.0"
|
lru = "0.12.0"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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,18 +260,20 @@ 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) =
|
||||||
Some(cell) => {
|
match cell {
|
||||||
let cell_data = DateCellData::from(&cell);
|
Some(cell) => {
|
||||||
(
|
let cell_data = DateCellData::from(&cell);
|
||||||
cell_data.timestamp,
|
(
|
||||||
cell_data.end_timestamp,
|
cell_data.timestamp,
|
||||||
cell_data.include_time,
|
cell_data.end_timestamp,
|
||||||
cell_data.is_range,
|
cell_data.include_time,
|
||||||
)
|
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) {
|
||||||
let cell_data = DateCellData {
|
let cell_data = DateCellData {
|
||||||
@ -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))
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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!(
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user