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:
parent
8105da1c2b
commit
baa7c8d826
@ -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/plugins/document/presentation/editor_plugins/mention/mention_block.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/presentation/notifications/widgets/notification_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.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:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/base.dart';
|
||||
import '../util/common_operations.dart';
|
||||
import '../util/editor_test_operations.dart';
|
||||
import '../util/expectation.dart';
|
||||
import '../util/keyboard.dart';
|
||||
|
||||
void main() {
|
||||
@ -35,7 +38,7 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Trigger iline action menu and type 'remind tomorrow'
|
||||
// Trigger inline action menu and type 'remind tomorrow'
|
||||
final tomorrow = await _insertReminderTomorrow(tester);
|
||||
|
||||
Node node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!;
|
||||
@ -43,7 +46,7 @@ void main() {
|
||||
node.delta!.first.attributes![MentionBlockKeys.mention];
|
||||
|
||||
expect(node.type, 'paragraph');
|
||||
expect(mentionAttr['type'], MentionType.reminder.name);
|
||||
expect(mentionAttr['type'], MentionType.date.name);
|
||||
expect(mentionAttr['date'], tomorrow.toIso8601String());
|
||||
|
||||
await tester.tap(
|
||||
@ -67,9 +70,57 @@ void main() {
|
||||
_dateWithTime(dateTimeSettings.timeFormat, tomorrow, time);
|
||||
|
||||
expect(node.type, 'paragraph');
|
||||
expect(mentionAttr['type'], MentionType.reminder.name);
|
||||
expect(mentionAttr['type'], MentionType.date.name);
|
||||
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_add_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/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -493,6 +496,30 @@ extension CommonOperations on WidgetTester {
|
||||
await tapEmoji(icon);
|
||||
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 {
|
||||
|
@ -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/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/reminder_selector.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
@ -76,6 +77,9 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
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 'common_operations.dart';
|
||||
import 'expectation.dart';
|
||||
@ -343,6 +347,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
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 {
|
||||
final findDateEditor = find.byType(EndTimeButton);
|
||||
final findToggle = find.byType(Toggle);
|
||||
|
@ -1,14 +1,18 @@
|
||||
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/editor_plugins/header/document_header_node_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/presentation/home/home_stack.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_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
@ -242,4 +246,29 @@ extension Expectation on WidgetTester {
|
||||
);
|
||||
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/mobile_calendar_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';
|
||||
|
||||
extension MobileRouter on BuildContext {
|
||||
Future<void> pushView(ViewPB view) async {
|
||||
Future<void> pushView(ViewPB view, [Map<String, dynamic>? arguments]) async {
|
||||
push(
|
||||
Uri(
|
||||
path: view.routeName,
|
||||
queryParameters: view.queryParameters,
|
||||
queryParameters: view.queryParameters(arguments),
|
||||
).toString(),
|
||||
).then((value) {
|
||||
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) {
|
||||
case ViewLayoutPB.Document:
|
||||
return {
|
||||
@ -47,6 +49,7 @@ extension on ViewPB {
|
||||
return {
|
||||
MobileGridScreen.viewId: id,
|
||||
MobileGridScreen.viewTitle: name,
|
||||
MobileGridScreen.viewArgs: jsonEncode(arguments),
|
||||
};
|
||||
case ViewLayoutPB.Calendar:
|
||||
return {
|
||||
|
@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.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/bottom_sheet.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/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/view/view_bloc.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:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@ -21,14 +24,16 @@ class MobileViewPage extends StatefulWidget {
|
||||
const MobileViewPage({
|
||||
super.key,
|
||||
required this.id,
|
||||
this.title,
|
||||
required this.viewLayout,
|
||||
this.title,
|
||||
this.arguments,
|
||||
});
|
||||
|
||||
/// view id
|
||||
final String id;
|
||||
final String? title;
|
||||
final ViewLayoutPB viewLayout;
|
||||
final String? title;
|
||||
final Map<String, dynamic>? arguments;
|
||||
|
||||
@override
|
||||
State<MobileViewPage> createState() => _MobileViewPageState();
|
||||
@ -40,7 +45,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
future = ViewBackendService.getView(widget.id);
|
||||
}
|
||||
|
||||
@ -67,7 +71,10 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
body = state.data!.fold((view) {
|
||||
viewPB = view;
|
||||
actions.add(_buildAppBarMoreButton(view));
|
||||
return view.plugin().widgetBuilder.buildWidget(shrinkWrap: false);
|
||||
return view
|
||||
.plugin(arguments: widget.arguments ?? const {})
|
||||
.widgetBuilder
|
||||
.buildWidget(shrinkWrap: false);
|
||||
}, (error) {
|
||||
return FlowyMobileStateContainer.error(
|
||||
emoji: '😔',
|
||||
@ -89,6 +96,10 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
create: (_) =>
|
||||
ViewBloc(view: viewPB!)..add(const ViewEvent.initial()),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: getIt<ReminderBloc>()
|
||||
..add(const ReminderEvent.started()),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
@ -131,9 +142,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
leading: const AppBarBackButton(),
|
||||
actions: actions,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: child,
|
||||
),
|
||||
body: SafeArea(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/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/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/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:easy_localization/easy_localization.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:go_router/go_router.dart';
|
||||
|
||||
class MobileDateCellEditScreen extends StatefulWidget {
|
||||
static const routeName = '/edit_date_cell';
|
||||
|
||||
// the type is DateCellController
|
||||
static const dateCellController = 'date_cell_controller';
|
||||
|
||||
// bool value, default is true
|
||||
static const fullScreen = 'full_screen';
|
||||
|
||||
@ -38,20 +38,13 @@ class MobileDateCellEditScreen extends StatefulWidget {
|
||||
|
||||
class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen();
|
||||
}
|
||||
Widget build(BuildContext context) =>
|
||||
widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen();
|
||||
|
||||
Widget _buildFullScreen() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.titleBar_date.tr(),
|
||||
),
|
||||
),
|
||||
body: _DateCellEditBody(
|
||||
dateCellController: widget.controller,
|
||||
),
|
||||
appBar: AppBar(title: FlowyText.medium(LocaleKeys.titleBar_date.tr())),
|
||||
body: _buildDatePicker(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -71,353 +64,73 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: const Center(child: DragHandler()),
|
||||
),
|
||||
_buildHeader(),
|
||||
_DateCellEditBody(
|
||||
dateCellController: widget.controller,
|
||||
),
|
||||
const MobileDateHeader(),
|
||||
_buildDatePicker(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
const iconWidth = 30.0;
|
||||
const height = 44.0;
|
||||
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(iconWidth),
|
||||
),
|
||||
width: iconWidth,
|
||||
iconPadding: EdgeInsets.zero,
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
Widget _buildDatePicker() => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<DateCellEditorBloc>(
|
||||
create: (_) => DateCellEditorBloc(
|
||||
reminderBloc: getIt<ReminderBloc>(),
|
||||
cellController: widget.controller,
|
||||
)..add(const DateCellEditorEvent.initial()),
|
||||
),
|
||||
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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Divider extends StatelessWidget {
|
||||
const _Divider();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const VSpace(20.0);
|
||||
}
|
||||
}
|
||||
|
||||
class _IncludeTimePicker extends StatefulWidget {
|
||||
const _IncludeTimePicker();
|
||||
|
||||
@override
|
||||
State<_IncludeTimePicker> createState() => _IncludeTimePickerState();
|
||||
}
|
||||
|
||||
class _IncludeTimePickerState extends State<_IncludeTimePicker> {
|
||||
String? _selectedTime;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
|
||||
builder: (context, state) {
|
||||
final startDay = state.dateStr;
|
||||
final endDay = state.endDateStr;
|
||||
final includeTime = state.includeTime;
|
||||
final use24hFormat =
|
||||
state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwentyFourHour;
|
||||
if (startDay == null || startDay.isEmpty) {
|
||||
return const Divider(
|
||||
height: 1,
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTime(
|
||||
context,
|
||||
includeTime,
|
||||
use24hFormat,
|
||||
true,
|
||||
startDay,
|
||||
state.timeStr,
|
||||
),
|
||||
VSpace(
|
||||
8.0,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
_buildTime(
|
||||
context,
|
||||
includeTime,
|
||||
use24hFormat,
|
||||
false,
|
||||
endDay,
|
||||
state.endTimeStr,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
),
|
||||
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
|
||||
builder: (context, state) {
|
||||
return MobileAppFlowyDatePicker(
|
||||
selectedDay: state.dateTime,
|
||||
dateStr: state.dateStr,
|
||||
endDateStr: state.endDateStr,
|
||||
timeStr: state.timeStr,
|
||||
endTimeStr: state.endTimeStr,
|
||||
startDay: state.startDay,
|
||||
endDay: state.endDay,
|
||||
enableRanges: true,
|
||||
isRange: state.isRange,
|
||||
includeTime: state.includeTime,
|
||||
use24hFormat: state.dateTypeOptionPB.timeFormat ==
|
||||
TimeFormatPB.TwentyFourHour,
|
||||
selectedReminderOption: state.reminderOption,
|
||||
onStartTimeChanged: (String? time) {
|
||||
if (time != null) {
|
||||
context
|
||||
.read<DateCellEditorBloc>()
|
||||
.add(DateCellEditorEvent.setTime(time));
|
||||
}
|
||||
},
|
||||
onEndTimeChanged: (String? time) {
|
||||
if (time != null) {
|
||||
context
|
||||
.read<DateCellEditorBloc>()
|
||||
.add(DateCellEditorEvent.setEndTime(time));
|
||||
}
|
||||
},
|
||||
onDaySelected: (selectedDay, focusedDay) => context
|
||||
.read<DateCellEditorBloc>()
|
||||
.add(DateCellEditorEvent.selectDay(selectedDay)),
|
||||
onRangeSelected: (start, end, focused) => context
|
||||
.read<DateCellEditorBloc>()
|
||||
.add(DateCellEditorEvent.selectDateRange(start, end)),
|
||||
onRangeChanged: (value) => context
|
||||
.read<DateCellEditorBloc>()
|
||||
.add(DateCellEditorEvent.setIsRange(value)),
|
||||
onIncludeTimeChanged: (value) => context
|
||||
.read<DateCellEditorBloc>()
|
||||
.add(DateCellEditorEvent.setIncludeTime(value)),
|
||||
onClearDate: () => context
|
||||
.read<DateCellEditorBloc>()
|
||||
.add(const DateCellEditorEvent.clearDate()),
|
||||
onReminderSelected: (option) => context
|
||||
.read<DateCellEditorBloc>()
|
||||
.add(DateCellEditorEvent.setReminderOption(option: option)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileGridScreen extends StatelessWidget {
|
||||
static const routeName = '/grid';
|
||||
static const viewId = 'id';
|
||||
static const viewTitle = 'title';
|
||||
static const viewArgs = 'arguments';
|
||||
|
||||
const MobileGridScreen({
|
||||
super.key,
|
||||
required this.id,
|
||||
this.title,
|
||||
this.arguments,
|
||||
});
|
||||
|
||||
/// view id
|
||||
final String id;
|
||||
final String? title;
|
||||
final Map<String, dynamic>? arguments;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MobileViewPage(
|
||||
id: id,
|
||||
title: title,
|
||||
viewLayout: ViewLayoutPB.Document,
|
||||
viewLayout: ViewLayoutPB.Grid,
|
||||
arguments: arguments,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ class _NotificationScreenContent extends StatelessWidget {
|
||||
);
|
||||
|
||||
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(
|
||||
ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),
|
||||
|
@ -18,12 +18,13 @@ final class DateCellBackendService {
|
||||
..rowId = rowId;
|
||||
|
||||
Future<Either<Unit, FlowyError>> update({
|
||||
required bool includeTime,
|
||||
required bool isRange,
|
||||
DateTime? date,
|
||||
String? time,
|
||||
DateTime? endDate,
|
||||
String? endTime,
|
||||
required includeTime,
|
||||
required isRange,
|
||||
String? reminderId,
|
||||
}) {
|
||||
final payload = DateChangesetPB.create()
|
||||
..cellId = cellId
|
||||
@ -44,6 +45,9 @@ final class DateCellBackendService {
|
||||
if (endTime != null) {
|
||||
payload.endTime = endTime;
|
||||
}
|
||||
if (reminderId != null) {
|
||||
payload.reminderId = reminderId;
|
||||
}
|
||||
|
||||
return DatabaseEventUpdateDateCell(payload).send();
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
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/locale_keys.g.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/widget/error_page.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 '../../widgets/card/card.dart';
|
||||
@ -30,6 +31,7 @@ import '../../widgets/card/card_cell_builder.dart';
|
||||
import '../../widgets/card/cells/card_cell.dart';
|
||||
import '../../widgets/row/cell_builder.dart';
|
||||
import '../application/board_bloc.dart';
|
||||
|
||||
import 'toolbar/board_setting_bar.dart';
|
||||
import 'widgets/board_hidden_groups.dart';
|
||||
|
||||
@ -40,6 +42,7 @@ class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
bool shrinkWrap,
|
||||
String? initialRowId,
|
||||
) =>
|
||||
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/locale_keys.g.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_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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 '../../widgets/row/cell_builder.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
|
||||
import 'calendar_day.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
@ -38,6 +40,7 @@ class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
bool shrinkWrap,
|
||||
String? initialRowId,
|
||||
) {
|
||||
return CalendarPage(
|
||||
key: _makeValueKey(controller),
|
||||
|
@ -1,9 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.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/tab_bar/desktop/setting_menu.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/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.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_scrollview.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/material.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_controller.dart';
|
||||
import '../application/grid_bloc.dart';
|
||||
import '../../application/database_controller.dart';
|
||||
import 'grid_scroll.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/sizes.dart';
|
||||
import 'widgets/row/row.dart';
|
||||
import 'widgets/footer/grid_footer.dart';
|
||||
import 'widgets/header/grid_header.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
import 'widgets/row/row.dart';
|
||||
import 'widgets/shortcuts.dart';
|
||||
|
||||
class ToggleExtensionNotifier extends ChangeNotifier {
|
||||
@ -49,11 +55,13 @@ class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
bool shrinkWrap,
|
||||
String? initialRowId,
|
||||
) {
|
||||
return GridPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
initialRowId: initialRowId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -85,31 +93,33 @@ class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
}
|
||||
|
||||
class GridPage extends StatefulWidget {
|
||||
final DatabaseController databaseController;
|
||||
const GridPage({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
this.onDeleted,
|
||||
super.key,
|
||||
this.initialRowId,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final DatabaseController databaseController;
|
||||
final VoidCallback? onDeleted;
|
||||
final String? initialRowId;
|
||||
|
||||
@override
|
||||
State<GridPage> createState() => _GridPageState();
|
||||
}
|
||||
|
||||
class _GridPageState extends State<GridPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
bool _didOpenInitialRow = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<NotificationActionBloc>.value(
|
||||
value: getIt<NotificationActionBloc>(),
|
||||
),
|
||||
BlocProvider<GridBloc>(
|
||||
create: (context) => GridBloc(
|
||||
view: widget.view,
|
||||
@ -117,35 +127,88 @@ class _GridPageState extends State<GridPage> {
|
||||
)..add(const GridEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<GridBloc, GridState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listener: (context, state) {
|
||||
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: (_) =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => GridShortcuts(
|
||||
child: GridPageContent(view: widget.view),
|
||||
),
|
||||
(_) => GridShortcuts(child: GridPageContent(view: widget.view)),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
),
|
||||
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 {
|
||||
final ViewPB view;
|
||||
const GridPageContent({
|
||||
required this.view,
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
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/locale_keys.g.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/presentation/widgets/shortcuts.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/protobuf/flowy-database2/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_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
||||
@ -33,11 +36,13 @@ class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
bool shrinkWrap,
|
||||
String? initialRowId,
|
||||
) {
|
||||
return MobileGridPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
initialRowId: initialRowId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -58,26 +63,33 @@ class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
}
|
||||
|
||||
class MobileGridPage extends StatefulWidget {
|
||||
final DatabaseController databaseController;
|
||||
const MobileGridPage({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
this.onDeleted,
|
||||
super.key,
|
||||
this.initialRowId,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final DatabaseController databaseController;
|
||||
final VoidCallback? onDeleted;
|
||||
final String? initialRowId;
|
||||
|
||||
@override
|
||||
State<MobileGridPage> createState() => _MobileGridPageState();
|
||||
}
|
||||
|
||||
class _MobileGridPageState extends State<MobileGridPage> {
|
||||
bool _didOpenInitialRow = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<NotificationActionBloc>.value(
|
||||
value: getIt<NotificationActionBloc>(),
|
||||
),
|
||||
BlocProvider<GridBloc>(
|
||||
create: (context) => GridBloc(
|
||||
view: widget.view,
|
||||
@ -90,19 +102,43 @@ class _MobileGridPageState extends State<MobileGridPage> {
|
||||
return state.loadingState.map(
|
||||
loading: (_) =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => GridShortcuts(child: GridPageContent(view: widget.view)),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
),
|
||||
finish: (result) {
|
||||
_openRow(context, widget.initialRowId, true);
|
||||
return result.successOrFail.fold(
|
||||
(_) => GridShortcuts(child: GridPageContent(view: widget.view)),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
);
|
||||
},
|
||||
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 {
|
||||
|
@ -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/locale_keys.g.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:flowy_infra/theme_extension.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:provider/provider.dart';
|
||||
|
||||
import '../../../../widgets/row/accessory/cell_accessory.dart';
|
||||
import '../../../../widgets/row/cells/cell_container.dart';
|
||||
import '../../layout/sizes.dart';
|
||||
|
||||
import 'action.dart';
|
||||
|
||||
class GridRow extends StatefulWidget {
|
||||
@ -186,15 +188,15 @@ class InsertRowButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class RowMenuButton extends StatefulWidget {
|
||||
final VoidCallback openMenu;
|
||||
final bool isDragEnabled;
|
||||
|
||||
const RowMenuButton({
|
||||
super.key,
|
||||
required this.openMenu,
|
||||
this.isDragEnabled = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final VoidCallback openMenu;
|
||||
final bool isDragEnabled;
|
||||
|
||||
@override
|
||||
State<RowMenuButton> createState() => _RowMenuButtonState();
|
||||
}
|
||||
@ -227,14 +229,15 @@ class _RowMenuButtonState extends State<RowMenuButton> {
|
||||
}
|
||||
|
||||
class RowContent extends StatelessWidget {
|
||||
final VoidCallback onExpand;
|
||||
final GridCellBuilder builder;
|
||||
const RowContent({
|
||||
super.key,
|
||||
required this.builder,
|
||||
required this.onExpand,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final GridCellBuilder builder;
|
||||
final VoidCallback onExpand;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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/locale_keys.g.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/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'tab_bar_add_button.dart';
|
||||
@ -37,9 +38,7 @@ class TabBarHeader extends StatelessWidget {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Flexible(
|
||||
child: DatabaseTabBar(),
|
||||
),
|
||||
const Flexible(child: DatabaseTabBar()),
|
||||
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
|
||||
builder: (context, state) {
|
||||
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/tab_bar_bloc.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_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'desktop/tab_bar_header.dart';
|
||||
@ -26,6 +27,7 @@ abstract class DatabaseTabBarItemBuilder {
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
bool shrinkWrap,
|
||||
String? initialRowId,
|
||||
);
|
||||
|
||||
/// 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 {
|
||||
final ViewPB view;
|
||||
final bool shrinkWrap;
|
||||
|
||||
/// Used to open a Row on plugin load
|
||||
///
|
||||
final String? initialRowId;
|
||||
|
||||
const DatabaseTabBarView({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.shrinkWrap,
|
||||
super.key,
|
||||
this.initialRowId,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -55,19 +63,12 @@ class DatabaseTabBarView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
|
||||
PageController? _pageController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController(
|
||||
initialPage: 0,
|
||||
);
|
||||
}
|
||||
final PageController _pageController = PageController(initialPage: 0);
|
||||
late String? _initialRowId = widget.initialRowId;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController?.dispose();
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -75,15 +76,14 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<DatabaseTabBarBloc>(
|
||||
create: (context) => DatabaseTabBarBloc(view: widget.view)
|
||||
..add(
|
||||
const DatabaseTabBarEvent.initial(),
|
||||
),
|
||||
..add(const DatabaseTabBarEvent.initial()),
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<DatabaseTabBarBloc, DatabaseTabBarState>(
|
||||
listenWhen: (p, c) => p.selectedIndex != c.selectedIndex,
|
||||
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>(
|
||||
builder: (context, state) {
|
||||
return pageSettingBarExtensionFromState(state);
|
||||
},
|
||||
builder: (context, state) =>
|
||||
pageSettingBarExtensionFromState(state),
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
|
||||
builder: (context, state) {
|
||||
return PageView(
|
||||
pageSnapping: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _pageController,
|
||||
children: pageContentFromState(state),
|
||||
);
|
||||
},
|
||||
builder: (context, state) => PageView(
|
||||
pageSnapping: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _pageController,
|
||||
children: pageContentFromState(state),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -146,11 +143,13 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
|
||||
return state.tabBars.map((tabBar) {
|
||||
final controller =
|
||||
state.tabBarControllerByViewId[tabBar.viewId]!.controller;
|
||||
|
||||
return tabBar.builder.content(
|
||||
context,
|
||||
tabBar.view,
|
||||
controller,
|
||||
widget.shrinkWrap,
|
||||
_initialRowId,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
@ -174,15 +173,21 @@ class DatabaseTabBarViewPlugin extends Plugin {
|
||||
final ViewPluginNotifier notifier;
|
||||
final PluginType _pluginType;
|
||||
|
||||
/// Used to open a Row on plugin load
|
||||
///
|
||||
final String? initialRowId;
|
||||
|
||||
DatabaseTabBarViewPlugin({
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
this.initialRowId,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(view: view);
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder(
|
||||
notifier: notifier,
|
||||
initialRowId: initialRowId,
|
||||
);
|
||||
|
||||
@override
|
||||
@ -195,9 +200,14 @@ class DatabaseTabBarViewPlugin extends Plugin {
|
||||
class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
final ViewPluginNotifier notifier;
|
||||
|
||||
/// Used to open a Row on plugin load
|
||||
///
|
||||
final String? initialRowId;
|
||||
|
||||
DatabasePluginWidgetBuilder({
|
||||
required this.notifier,
|
||||
Key? key,
|
||||
required this.notifier,
|
||||
this.initialRowId,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -219,6 +229,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
key: ValueKey(notifier.view.id),
|
||||
view: notifier.view,
|
||||
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/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import '../cell_builder.dart';
|
||||
|
||||
@ -52,14 +52,15 @@ abstract mixin class GridCellAccessoryState {
|
||||
}
|
||||
|
||||
class PrimaryCellAccessory extends StatefulWidget {
|
||||
final VoidCallback onTapCallback;
|
||||
final bool isCellEditing;
|
||||
const PrimaryCellAccessory({
|
||||
super.key,
|
||||
required this.onTapCallback,
|
||||
required this.isCellEditing,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final VoidCallback onTapCallback;
|
||||
final bool isCellEditing;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PrimaryCellAccessoryState();
|
||||
}
|
||||
|
@ -1,14 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../grid/presentation/layout/sizes.dart';
|
||||
import '../../cell_builder.dart';
|
||||
|
||||
import 'date_cell_bloc.dart';
|
||||
import 'date_editor.dart';
|
||||
|
||||
@ -85,22 +91,32 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
child: Container(
|
||||
alignment: alignment,
|
||||
padding: padding,
|
||||
child: FlowyText.medium(
|
||||
text,
|
||||
color: color,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText.medium(
|
||||
text,
|
||||
color: color,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (state.data?.reminderId.isNotEmpty == true) ...[
|
||||
const HSpace(5),
|
||||
FlowyTooltip(
|
||||
message:
|
||||
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
|
||||
child: const FlowySvg(FlowySvgs.clock_alarm_s),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContent) {
|
||||
return DateCellEditor(
|
||||
cellController: _cellController,
|
||||
onDismissed: () =>
|
||||
widget.cellContainerNotifier.isFocus = false,
|
||||
);
|
||||
},
|
||||
onClose: () {
|
||||
widget.cellContainerNotifier.isFocus = false;
|
||||
},
|
||||
popupBuilder: (_) => DateCellEditor(
|
||||
cellController: _cellController,
|
||||
onDismissed: () => widget.cellContainerNotifier.isFocus = false,
|
||||
),
|
||||
onClose: () => widget.cellContainerNotifier.isFocus = false,
|
||||
);
|
||||
} else if (widget.cellStyle.useRoundedBorder) {
|
||||
return InkWell(
|
||||
@ -108,12 +124,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
padding: EdgeInsets.zero,
|
||||
builder: (context) {
|
||||
return MobileDateCellEditScreen(
|
||||
controller: _cellController,
|
||||
showAsFullScreen: false,
|
||||
);
|
||||
},
|
||||
builder: (_) => MobileDateCellEditScreen(
|
||||
controller: _cellController,
|
||||
showAsFullScreen: false,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
@ -146,28 +160,36 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: FlowyText(
|
||||
text,
|
||||
color: color,
|
||||
fontSize: 15,
|
||||
maxLines: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
if (state.data?.reminderId.isNotEmpty == true) ...[
|
||||
FlowyTooltip(
|
||||
message:
|
||||
LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
|
||||
child: const FlowySvg(FlowySvgs.clock_alarm_s),
|
||||
),
|
||||
const HSpace(5),
|
||||
],
|
||||
FlowyText(
|
||||
text,
|
||||
color: color,
|
||||
fontSize: 15,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
builder: (context) {
|
||||
return MobileDateCellEditScreen(
|
||||
controller: _cellController,
|
||||
showAsFullScreen: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
builder: (_) => MobileDateCellEditScreen(
|
||||
controller: _cellController,
|
||||
showAsFullScreen: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -5,15 +5,22 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
|
||||
import 'package:appflowy/plugins/database/application/cell/date_cell_service.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/util/int64_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart'
|
||||
show StringTranslateExtension;
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:nanoid/non_secure.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'date_cell_editor_bloc.freezed.dart';
|
||||
@ -22,16 +29,19 @@ class DateCellEditorBloc
|
||||
extends Bloc<DateCellEditorEvent, DateCellEditorState> {
|
||||
final DateCellBackendService _dateCellBackendService;
|
||||
final DateCellController cellController;
|
||||
final ReminderBloc _reminderBloc;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
DateCellEditorBloc({
|
||||
required this.cellController,
|
||||
}) : _dateCellBackendService = DateCellBackendService(
|
||||
required ReminderBloc reminderBloc,
|
||||
}) : _reminderBloc = reminderBloc,
|
||||
_dateCellBackendService = DateCellBackendService(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
rowId: cellController.rowId,
|
||||
),
|
||||
super(DateCellEditorState.initial(cellController)) {
|
||||
super(DateCellEditorState.initial(cellController, reminderBloc)) {
|
||||
on<DateCellEditorEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
@ -42,6 +52,41 @@ class DateCellEditorBloc
|
||||
dateCellData.isRange == state.isRange && dateCellData.isRange
|
||||
? dateCellData.endDateTime
|
||||
: null;
|
||||
|
||||
if (dateCellData.dateTime != null &&
|
||||
(state.reminderId?.isEmpty ?? true) &&
|
||||
(dateCellData.reminderId?.isNotEmpty ?? false) &&
|
||||
state.reminderOption != ReminderOption.none) {
|
||||
// Add Reminder
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.addById(
|
||||
reminderId: dateCellData.reminderId!,
|
||||
objectId: cellController.viewId,
|
||||
meta: {ReminderMetaKeys.rowId: cellController.rowId},
|
||||
scheduledAt: Int64(
|
||||
dateCellData.dateTime!
|
||||
.subtract(state.reminderOption.time)
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ((dateCellData.reminderId?.isNotEmpty ?? false) &&
|
||||
dateCellData.dateTime != null) {
|
||||
// Update Reminder
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(
|
||||
id: state.reminderId!,
|
||||
scheduledAt: dateCellData.dateTime!
|
||||
.subtract(state.reminderOption.time),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
dateTime: dateCellData.dateTime,
|
||||
@ -54,11 +99,14 @@ class DateCellEditorBloc
|
||||
endDay: endDay,
|
||||
dateStr: dateCellData.dateStr,
|
||||
endDateStr: dateCellData.endDateStr,
|
||||
reminderId: dateCellData.reminderId,
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveTimeFormatError:
|
||||
(String? parseTimeError, String? parseEndTimeError) {
|
||||
didReceiveTimeFormatError: (
|
||||
String? parseTimeError,
|
||||
String? parseEndTimeError,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
parseTimeError: parseTimeError,
|
||||
@ -67,17 +115,14 @@ class DateCellEditorBloc
|
||||
);
|
||||
},
|
||||
selectDay: (date) async {
|
||||
if (state.isRange) {
|
||||
return;
|
||||
if (!state.isRange) {
|
||||
await _updateDateData(date: date);
|
||||
}
|
||||
await _updateDateData(date: date);
|
||||
},
|
||||
setIncludeTime: (includeTime) async {
|
||||
await _updateDateData(includeTime: includeTime);
|
||||
},
|
||||
setIsRange: (isRange) async {
|
||||
await _updateDateData(isRange: isRange);
|
||||
},
|
||||
setIncludeTime: (includeTime) async =>
|
||||
await _updateDateData(includeTime: includeTime),
|
||||
setIsRange: (isRange) async =>
|
||||
await _updateDateData(isRange: isRange),
|
||||
setTime: (timeStr) async {
|
||||
emit(state.copyWith(timeStr: timeStr));
|
||||
await _updateDateData(timeStr: timeStr);
|
||||
@ -87,89 +132,88 @@ class DateCellEditorBloc
|
||||
final (newStart, newEnd) = state.startDay!.isBefore(start!)
|
||||
? (state.startDay!, start)
|
||||
: (start, state.startDay!);
|
||||
emit(
|
||||
state.copyWith(
|
||||
startDay: null,
|
||||
endDay: null,
|
||||
),
|
||||
);
|
||||
await _updateDateData(
|
||||
date: newStart.date,
|
||||
endDate: newEnd.date,
|
||||
);
|
||||
|
||||
emit(state.copyWith(startDay: null, endDay: null));
|
||||
|
||||
await _updateDateData(date: newStart.date, endDate: newEnd.date);
|
||||
} else if (end == null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
startDay: start,
|
||||
endDay: null,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(startDay: start, endDay: null));
|
||||
} else {
|
||||
await _updateDateData(
|
||||
date: start!.date,
|
||||
endDate: end.date,
|
||||
);
|
||||
await _updateDateData(date: start!.date, endDate: end.date);
|
||||
}
|
||||
},
|
||||
setStartDay: (DateTime startDay) async {
|
||||
if (state.endDay == null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
startDay: startDay,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(startDay: startDay));
|
||||
} else if (startDay.isAfter(state.endDay!)) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
startDay: startDay,
|
||||
endDay: null,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(startDay: startDay, endDay: null));
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
startDay: startDay,
|
||||
),
|
||||
emit(state.copyWith(startDay: startDay));
|
||||
await _updateDateData(
|
||||
date: startDay.date,
|
||||
endDate: state.endDay!.date,
|
||||
);
|
||||
_updateDateData(date: startDay.date, endDate: state.endDay!.date);
|
||||
}
|
||||
},
|
||||
setEndDay: (DateTime endDay) async {
|
||||
setEndDay: (DateTime endDay) {
|
||||
if (state.startDay == null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
endDay: endDay,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(endDay: endDay));
|
||||
} else if (endDay.isBefore(state.startDay!)) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
startDay: null,
|
||||
endDay: endDay,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(startDay: null, endDay: endDay));
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
endDay: endDay,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(endDay: endDay));
|
||||
_updateDateData(date: state.startDay!.date, endDate: endDay.date);
|
||||
}
|
||||
},
|
||||
setEndTime: (String endTime) async {
|
||||
setEndTime: (String? endTime) async {
|
||||
emit(state.copyWith(endTimeStr: endTime));
|
||||
await _updateDateData(endTimeStr: endTime);
|
||||
},
|
||||
setDateFormat: (dateFormat) async {
|
||||
await _updateTypeOption(emit, dateFormat: dateFormat);
|
||||
},
|
||||
setTimeFormat: (timeFormat) async {
|
||||
await _updateTypeOption(emit, timeFormat: timeFormat);
|
||||
},
|
||||
setDateFormat: (DateFormatPB dateFormat) async =>
|
||||
await _updateTypeOption(emit, dateFormat: dateFormat),
|
||||
setTimeFormat: (TimeFormatPB timeFormat) async =>
|
||||
await _updateTypeOption(emit, timeFormat: timeFormat),
|
||||
clearDate: () async {
|
||||
// Remove reminder if neccessary
|
||||
if (state.reminderId != null) {
|
||||
_reminderBloc
|
||||
.add(ReminderEvent.remove(reminderId: state.reminderId!));
|
||||
}
|
||||
|
||||
await _clearDate();
|
||||
},
|
||||
setReminderOption: (ReminderOption option) async {
|
||||
if (state.reminderId?.isEmpty ??
|
||||
true &&
|
||||
state.dateTime != null &&
|
||||
option != ReminderOption.none) {
|
||||
// New Reminder
|
||||
final reminderId = nanoid();
|
||||
await _updateDateData(reminderId: reminderId);
|
||||
|
||||
emit(state.copyWith(reminderOption: option));
|
||||
} else if (option == ReminderOption.none &&
|
||||
(state.reminderId?.isNotEmpty ?? false)) {
|
||||
// Remove reminder
|
||||
_reminderBloc
|
||||
.add(ReminderEvent.remove(reminderId: state.reminderId!));
|
||||
await _updateDateData(reminderId: "");
|
||||
emit(state.copyWith(reminderOption: option));
|
||||
} else if (state.dateTime != null &&
|
||||
(state.reminderId?.isNotEmpty ?? false)) {
|
||||
// Update reminder
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(
|
||||
id: state.reminderId!,
|
||||
scheduledAt: state.dateTime!.subtract(option.time),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
// Empty String signifies no reminder
|
||||
removeReminder: () async => await _updateDateData(reminderId: ""),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -182,6 +226,7 @@ class DateCellEditorBloc
|
||||
String? endTimeStr,
|
||||
bool? includeTime,
|
||||
bool? isRange,
|
||||
String? reminderId,
|
||||
}) async {
|
||||
// make sure that not both date and time are updated at the same time
|
||||
assert(
|
||||
@ -191,21 +236,15 @@ class DateCellEditorBloc
|
||||
|
||||
// if not updating the time, use the old time in the state
|
||||
final String? newTime = timeStr ?? state.timeStr;
|
||||
DateTime? newDate;
|
||||
if (timeStr != null && timeStr.isNotEmpty) {
|
||||
newDate = state.dateTime ?? DateTime.now();
|
||||
} else {
|
||||
newDate = _utcToLocalAndAddCurrentTime(date);
|
||||
}
|
||||
final DateTime? newDate = timeStr != null && timeStr.isNotEmpty
|
||||
? state.dateTime ?? DateTime.now()
|
||||
: _utcToLocalAndAddCurrentTime(date);
|
||||
|
||||
// if not updating the time, use the old time in the state
|
||||
final String? newEndTime = endTimeStr ?? state.endTimeStr;
|
||||
DateTime? newEndDate;
|
||||
if (endTimeStr != null && endTimeStr.isNotEmpty) {
|
||||
newEndDate = state.endDateTime ?? DateTime.now();
|
||||
} else {
|
||||
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
|
||||
}
|
||||
final DateTime? newEndDate = endTimeStr != null && endTimeStr.isNotEmpty
|
||||
? state.endDateTime ?? DateTime.now()
|
||||
: _utcToLocalAndAddCurrentTime(endDate);
|
||||
|
||||
final result = await _dateCellBackendService.update(
|
||||
date: newDate,
|
||||
@ -214,15 +253,14 @@ class DateCellEditorBloc
|
||||
endTime: newEndTime,
|
||||
includeTime: includeTime ?? state.includeTime,
|
||||
isRange: isRange ?? state.isRange,
|
||||
reminderId: reminderId ?? state.reminderId,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(_) {
|
||||
if (!isClosed &&
|
||||
(state.parseEndTimeError != null || state.parseTimeError != null)) {
|
||||
add(
|
||||
const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
|
||||
);
|
||||
add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null));
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
@ -231,10 +269,12 @@ class DateCellEditorBloc
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// to determine which textfield should show error
|
||||
final (startError, endError) = newDate != null
|
||||
? (timeFormatPrompt(err), null)
|
||||
: (null, timeFormatPrompt(err));
|
||||
|
||||
add(
|
||||
DateCellEditorEvent.didReceiveTimeFormatError(
|
||||
startError,
|
||||
@ -253,13 +293,9 @@ class DateCellEditorBloc
|
||||
final result = await _dateCellBackendService.clear();
|
||||
result.fold(
|
||||
(_) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
if (!isClosed) {
|
||||
add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null));
|
||||
}
|
||||
|
||||
add(
|
||||
const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
|
||||
);
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
@ -304,11 +340,11 @@ class DateCellEditorBloc
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((cell) {
|
||||
onCellChanged: (cell) {
|
||||
if (!isClosed) {
|
||||
add(DateCellEditorEvent.didReceiveCellUpdate(cell));
|
||||
}
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -335,7 +371,7 @@ class DateCellEditorBloc
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(l) => emit(
|
||||
(_) => emit(
|
||||
state.copyWith(
|
||||
dateTypeOptionPB: newDateTypeOption,
|
||||
timeHintText: _timeHintText(newDateTypeOption),
|
||||
@ -355,6 +391,7 @@ class DateCellEditorEvent with _$DateCellEditorEvent {
|
||||
const factory DateCellEditorEvent.didReceiveCellUpdate(
|
||||
DateCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
|
||||
const factory DateCellEditorEvent.didReceiveTimeFormatError(
|
||||
String? parseTimeError,
|
||||
String? parseEndTimeError,
|
||||
@ -362,27 +399,41 @@ class DateCellEditorEvent with _$DateCellEditorEvent {
|
||||
|
||||
// date cell data is modified
|
||||
const factory DateCellEditorEvent.selectDay(DateTime day) = _SelectDay;
|
||||
|
||||
const factory DateCellEditorEvent.selectDateRange(
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
) = _SelectDateRange;
|
||||
|
||||
const factory DateCellEditorEvent.setStartDay(
|
||||
DateTime startDay,
|
||||
) = _SetStartDay;
|
||||
|
||||
const factory DateCellEditorEvent.setEndDay(
|
||||
DateTime endDay,
|
||||
) = _SetEndDay;
|
||||
const factory DateCellEditorEvent.setTime(String time) = _Time;
|
||||
const factory DateCellEditorEvent.setEndTime(String endTime) = _EndTime;
|
||||
|
||||
const factory DateCellEditorEvent.setTime(String time) = _SetTime;
|
||||
|
||||
const factory DateCellEditorEvent.setEndTime(String endTime) = _SetEndTime;
|
||||
|
||||
const factory DateCellEditorEvent.setIncludeTime(bool includeTime) =
|
||||
_IncludeTime;
|
||||
const factory DateCellEditorEvent.setIsRange(bool isRange) = _IsRange;
|
||||
|
||||
const factory DateCellEditorEvent.setIsRange(bool isRange) = _SetIsRange;
|
||||
|
||||
const factory DateCellEditorEvent.setReminderOption({
|
||||
required ReminderOption option,
|
||||
}) = _SetReminderOption;
|
||||
|
||||
const factory DateCellEditorEvent.removeReminder() = _RemoveReminder;
|
||||
|
||||
// date field type options are modified
|
||||
const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) =
|
||||
_TimeFormat;
|
||||
_SetTimeFormat;
|
||||
|
||||
const factory DateCellEditorEvent.setDateFormat(DateFormatPB dateFormat) =
|
||||
_DateFormat;
|
||||
_SetDateFormat;
|
||||
|
||||
const factory DateCellEditorEvent.clearDate() = _ClearDate;
|
||||
}
|
||||
@ -406,17 +457,36 @@ class DateCellEditorState with _$DateCellEditorState {
|
||||
required bool isRange,
|
||||
required String? dateStr,
|
||||
required String? endDateStr,
|
||||
required String? reminderId,
|
||||
|
||||
// error and hint text
|
||||
required String? parseTimeError,
|
||||
required String? parseEndTimeError,
|
||||
required String timeHintText,
|
||||
@Default(ReminderOption.none) ReminderOption reminderOption,
|
||||
}) = _DateCellEditorState;
|
||||
|
||||
factory DateCellEditorState.initial(DateCellController controller) {
|
||||
factory DateCellEditorState.initial(
|
||||
DateCellController controller,
|
||||
ReminderBloc reminderBloc,
|
||||
) {
|
||||
final typeOption = controller.getTypeOption(DateTypeOptionDataParser());
|
||||
final cellData = controller.getCellData();
|
||||
final dateCellData = _dateDataFromCellData(cellData);
|
||||
|
||||
ReminderOption reminderOption = ReminderOption.none;
|
||||
if ((dateCellData.reminderId?.isNotEmpty ?? false) &&
|
||||
dateCellData.dateTime != null) {
|
||||
final reminder = reminderBloc.state.reminders
|
||||
.firstWhereOrNull((r) => r.id == dateCellData.reminderId);
|
||||
if (reminder != null) {
|
||||
reminderOption = ReminderOption.fromDateDifference(
|
||||
dateCellData.dateTime!,
|
||||
reminder.scheduledAt.toDateTime(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return DateCellEditorState(
|
||||
dateTypeOptionPB: typeOption,
|
||||
startDay: dateCellData.isRange ? dateCellData.dateTime : null,
|
||||
@ -432,6 +502,8 @@ class DateCellEditorState with _$DateCellEditorState {
|
||||
parseTimeError: null,
|
||||
parseEndTimeError: null,
|
||||
timeHintText: _timeHintText(typeOption),
|
||||
reminderId: dateCellData.reminderId,
|
||||
reminderOption: reminderOption,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -462,6 +534,7 @@ _DateCellData _dateDataFromCellData(
|
||||
isRange: false,
|
||||
dateStr: null,
|
||||
endDateStr: null,
|
||||
reminderId: null,
|
||||
);
|
||||
}
|
||||
|
||||
@ -481,12 +554,14 @@ _DateCellData _dateDataFromCellData(
|
||||
endTimeStr = cellData.endTime;
|
||||
}
|
||||
}
|
||||
|
||||
final bool includeTime = cellData.includeTime;
|
||||
final bool isRange = cellData.isRange;
|
||||
|
||||
if (cellData.isRange) {
|
||||
endDateStr = cellData.endDate;
|
||||
}
|
||||
|
||||
final String dateStr = cellData.date;
|
||||
|
||||
return _DateCellData(
|
||||
@ -498,6 +573,7 @@ _DateCellData _dateDataFromCellData(
|
||||
isRange: isRange,
|
||||
dateStr: dateStr,
|
||||
endDateStr: endDateStr,
|
||||
reminderId: cellData.reminderId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -510,6 +586,7 @@ class _DateCellData {
|
||||
final bool isRange;
|
||||
final String? dateStr;
|
||||
final String? endDateStr;
|
||||
final String? reminderId;
|
||||
|
||||
_DateCellData({
|
||||
required this.dateTime,
|
||||
@ -520,5 +597,6 @@ class _DateCellData {
|
||||
required this.isRange,
|
||||
required this.dateStr,
|
||||
required this.endDateStr,
|
||||
required this.reminderId,
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'date_cell_editor_bloc.dart';
|
||||
@ -31,20 +36,28 @@ class _DateCellEditor extends State<DateCellEditor> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => DateCellEditorBloc(
|
||||
cellController: widget.cellController,
|
||||
)..add(const DateCellEditorEvent.initial()),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<DateCellEditorBloc>(
|
||||
create: (context) => DateCellEditorBloc(
|
||||
reminderBloc: getIt<ReminderBloc>(),
|
||||
cellController: widget.cellController,
|
||||
)..add(const DateCellEditorEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<DateCellEditorBloc>();
|
||||
final dateCellBloc = context.read<DateCellEditorBloc>();
|
||||
return AppFlowyDatePicker(
|
||||
includeTime: state.includeTime,
|
||||
rebuildOnDaySelected: false,
|
||||
onIncludeTimeChanged: (value) =>
|
||||
bloc.add(DateCellEditorEvent.setIncludeTime(!value)),
|
||||
dateCellBloc.add(DateCellEditorEvent.setIncludeTime(!value)),
|
||||
isRange: state.isRange,
|
||||
startDay: state.isRange ? state.startDay : null,
|
||||
endDay: state.isRange ? state.endDay : null,
|
||||
onIsRangeChanged: (value) =>
|
||||
bloc.add(DateCellEditorEvent.setIsRange(!value)),
|
||||
dateCellBloc.add(DateCellEditorEvent.setIsRange(!value)),
|
||||
dateFormat: state.dateTypeOptionPB.dateFormat,
|
||||
timeFormat: state.dateTypeOptionPB.timeFormat,
|
||||
selectedDay: state.dateTime,
|
||||
@ -54,28 +67,36 @@ class _DateCellEditor extends State<DateCellEditor> {
|
||||
parseEndTimeError: state.parseEndTimeError,
|
||||
parseTimeError: state.parseTimeError,
|
||||
popoverMutex: popoverMutex,
|
||||
onStartTimeSubmitted: (timeStr) {
|
||||
bloc.add(DateCellEditorEvent.setTime(timeStr));
|
||||
},
|
||||
onEndTimeSubmitted: (timeStr) {
|
||||
bloc.add(DateCellEditorEvent.setEndTime(timeStr));
|
||||
},
|
||||
onDaySelected: (selectedDay, _) {
|
||||
bloc.add(DateCellEditorEvent.selectDay(selectedDay));
|
||||
},
|
||||
onRangeSelected: (start, end, _) {
|
||||
bloc.add(DateCellEditorEvent.selectDateRange(start, end));
|
||||
},
|
||||
allowFormatChanges: true,
|
||||
onDateFormatChanged: (format) {
|
||||
bloc.add(DateCellEditorEvent.setDateFormat(format));
|
||||
},
|
||||
onTimeFormatChanged: (format) {
|
||||
bloc.add(DateCellEditorEvent.setTimeFormat(format));
|
||||
},
|
||||
onClearDate: () {
|
||||
bloc.add(const DateCellEditorEvent.clearDate());
|
||||
},
|
||||
onReminderSelected: (option) => dateCellBloc
|
||||
.add(DateCellEditorEvent.setReminderOption(option: option)),
|
||||
selectedReminderOption: state.reminderOption,
|
||||
options: [
|
||||
OptionGroup(
|
||||
options: [
|
||||
DateTypeOptionButton(
|
||||
popoverMutex: popoverMutex,
|
||||
dateFormat: state.dateTypeOptionPB.dateFormat,
|
||||
timeFormat: state.dateTypeOptionPB.timeFormat,
|
||||
onDateFormatChanged: (format) => dateCellBloc
|
||||
.add(DateCellEditorEvent.setDateFormat(format)),
|
||||
onTimeFormatChanged: (format) => dateCellBloc
|
||||
.add(DateCellEditorEvent.setTimeFormat(format)),
|
||||
),
|
||||
ClearDateButton(
|
||||
onClearDate: () =>
|
||||
dateCellBloc.add(const DateCellEditorEvent.clearDate()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onStartTimeSubmitted: (timeStr) =>
|
||||
dateCellBloc.add(DateCellEditorEvent.setTime(timeStr)),
|
||||
onEndTimeSubmitted: (timeStr) =>
|
||||
dateCellBloc.add(DateCellEditorEvent.setEndTime(timeStr)),
|
||||
onDaySelected: (selectedDay, _) =>
|
||||
dateCellBloc.add(DateCellEditorEvent.selectDay(selectedDay)),
|
||||
onRangeSelected: (start, end, _) => dateCellBloc
|
||||
.add(DateCellEditorEvent.selectDateRange(start, end)),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -1,191 +0,0 @@
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
import 'date_cell_editor_bloc.dart';
|
||||
|
||||
class MobileDatePicker extends StatefulWidget {
|
||||
const MobileDatePicker({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileDatePicker> createState() => _MobileDatePickerState();
|
||||
}
|
||||
|
||||
class _MobileDatePickerState extends State<MobileDatePicker> {
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||
|
||||
final ValueNotifier<(DateTime, dynamic)> _currentDateNotifier = ValueNotifier(
|
||||
(DateTime.now(), null),
|
||||
);
|
||||
PageController? _pageController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const VSpace(8.0),
|
||||
_buildHeader(context),
|
||||
const VSpace(8.0),
|
||||
_buildCalendar(context),
|
||||
const VSpace(16.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendar(BuildContext context) {
|
||||
const selectedColor = Color(0xFF00BCF0);
|
||||
final textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith();
|
||||
const boxDecoration = BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
|
||||
builder: (context, state) {
|
||||
return TableCalendar(
|
||||
firstDay: kFirstDay,
|
||||
lastDay: kLastDay,
|
||||
focusedDay: _focusedDay,
|
||||
rowHeight: 48.0,
|
||||
calendarFormat: _calendarFormat,
|
||||
daysOfWeekHeight: 48.0,
|
||||
rangeSelectionMode: state.isRange
|
||||
? RangeSelectionMode.enforced
|
||||
: RangeSelectionMode.disabled,
|
||||
rangeStartDay: state.isRange ? state.startDay : null,
|
||||
rangeEndDay: state.isRange ? state.endDay : null,
|
||||
onCalendarCreated: (pageController) =>
|
||||
_pageController = pageController,
|
||||
headerVisible: false,
|
||||
availableGestures: AvailableGestures.horizontalSwipe,
|
||||
calendarStyle: CalendarStyle(
|
||||
cellMargin: const EdgeInsets.all(3.5),
|
||||
defaultDecoration: boxDecoration,
|
||||
selectedDecoration: boxDecoration.copyWith(
|
||||
color: selectedColor,
|
||||
),
|
||||
todayDecoration: boxDecoration.copyWith(
|
||||
color: Colors.transparent,
|
||||
border: Border.all(color: selectedColor),
|
||||
),
|
||||
weekendDecoration: boxDecoration,
|
||||
outsideDecoration: boxDecoration,
|
||||
rangeStartDecoration: boxDecoration.copyWith(
|
||||
color: selectedColor,
|
||||
),
|
||||
rangeEndDecoration: boxDecoration.copyWith(
|
||||
color: selectedColor,
|
||||
),
|
||||
defaultTextStyle: textStyle,
|
||||
weekendTextStyle: textStyle,
|
||||
selectedTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
rangeStartTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
rangeEndTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
todayTextStyle: textStyle,
|
||||
outsideTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
rangeHighlightColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
calendarBuilders: CalendarBuilders(
|
||||
dowBuilder: (context, day) {
|
||||
final locale = context.locale.toLanguageTag();
|
||||
final label = DateFormat.E(locale).format(day).substring(0, 2);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: textStyle.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
selectedDayPredicate: (day) =>
|
||||
state.isRange ? false : isSameDay(state.dateTime, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
context.read<DateCellEditorBloc>().add(
|
||||
DateCellEditorEvent.selectDay(selectedDay),
|
||||
);
|
||||
},
|
||||
onRangeSelected: (start, end, focusedDay) {
|
||||
context.read<DateCellEditorBloc>().add(
|
||||
DateCellEditorEvent.selectDateRange(start, end),
|
||||
);
|
||||
},
|
||||
onFormatChanged: (calendarFormat) => setState(() {
|
||||
_calendarFormat = calendarFormat;
|
||||
}),
|
||||
onPageChanged: (focusedDay) => setState(() {
|
||||
_focusedDay = focusedDay;
|
||||
_currentDateNotifier.value = (focusedDay, null);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
const HSpace(16.0),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _currentDateNotifier,
|
||||
builder: (_, value, ___) {
|
||||
return FlowyText(
|
||||
DateFormat.yMMMM(value.$2).format(value.$1),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowySvg(
|
||||
FlowySvgs.arrow_left_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(24.0),
|
||||
),
|
||||
onTap: () => _pageController?.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
const HSpace(24.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowySvg(
|
||||
FlowySvgs.arrow_right_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(24.0),
|
||||
),
|
||||
onTap: () => _pageController?.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
library document_plugin;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/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/view_title_bar.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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentPluginBuilder extends PluginBuilder {
|
||||
@ -22,9 +24,9 @@ class DocumentPluginBuilder extends PluginBuilder {
|
||||
Plugin build(dynamic data) {
|
||||
if (data is ViewPB) {
|
||||
return DocumentPlugin(pluginType: pluginType, view: data);
|
||||
} else {
|
||||
throw FlowyPluginException.invalidData;
|
||||
}
|
||||
|
||||
throw FlowyPluginException.invalidData;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -41,26 +43,28 @@ class DocumentPluginBuilder extends PluginBuilder {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
|
||||
DocumentPlugin({
|
||||
required PluginType pluginType,
|
||||
required ViewPB view,
|
||||
bool listenOnViewChanged = false,
|
||||
Key? key,
|
||||
}) : notifier = ViewPluginNotifier(view: view) {
|
||||
_pluginType = pluginType;
|
||||
}
|
||||
final Selection? initialSelection;
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder {
|
||||
return DocumentPluginWidgetBuilder(
|
||||
notifier: notifier,
|
||||
);
|
||||
}
|
||||
PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder(
|
||||
notifier: notifier,
|
||||
initialSelection: initialSelection,
|
||||
);
|
||||
|
||||
@override
|
||||
PluginType get pluginType => _pluginType;
|
||||
@ -71,14 +75,16 @@ class DocumentPlugin extends Plugin<int> {
|
||||
|
||||
class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
with NavigationItem {
|
||||
DocumentPluginWidgetBuilder({
|
||||
Key? key,
|
||||
required this.notifier,
|
||||
this.initialSelection,
|
||||
});
|
||||
|
||||
final ViewPluginNotifier notifier;
|
||||
ViewPB get view => notifier.view;
|
||||
int? deletedViewIndex;
|
||||
|
||||
DocumentPluginWidgetBuilder({
|
||||
required this.notifier,
|
||||
Key? key,
|
||||
});
|
||||
final Selection? initialSelection;
|
||||
|
||||
@override
|
||||
EdgeInsets get contentPadding => EdgeInsets.zero;
|
||||
@ -86,21 +92,23 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
@override
|
||||
Widget buildWidget({PluginContext? context, required bool shrinkWrap}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
||||
if (deletedView.hasIndex()) {
|
||||
deletedViewIndex = deletedView.index;
|
||||
}
|
||||
});
|
||||
notifier.isDeleted.value.fold(
|
||||
() => null,
|
||||
(deletedView) {
|
||||
if (deletedView.hasIndex()) {
|
||||
deletedViewIndex = deletedView.index;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (_, state) {
|
||||
return DocumentPage(
|
||||
view: view,
|
||||
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
|
||||
key: ValueKey(view.id),
|
||||
);
|
||||
},
|
||||
builder: (_, state) => DocumentPage(
|
||||
key: ValueKey(view.id),
|
||||
view: view,
|
||||
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
|
||||
initialSelection: initialSelection,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -114,10 +122,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget? get rightBarItem {
|
||||
return Row(
|
||||
children: [
|
||||
DocumentShareButton(
|
||||
key: ValueKey(view.id),
|
||||
view: view,
|
||||
),
|
||||
DocumentShareButton(key: ValueKey(view.id), view: view),
|
||||
const HSpace(4),
|
||||
const DocumentMoreButton(),
|
||||
],
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.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:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
enum EditorNotificationType {
|
||||
@ -35,12 +36,14 @@ class EditorNotification extends Notification {
|
||||
class DocumentPage extends StatefulWidget {
|
||||
const DocumentPage({
|
||||
super.key,
|
||||
required this.onDeleted,
|
||||
required this.view,
|
||||
required this.onDeleted,
|
||||
this.initialSelection,
|
||||
});
|
||||
|
||||
final VoidCallback onDeleted;
|
||||
final ViewPB view;
|
||||
final VoidCallback onDeleted;
|
||||
final Selection? initialSelection;
|
||||
|
||||
@override
|
||||
State<DocumentPage> createState() => _DocumentPageState();
|
||||
@ -88,10 +91,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
|
||||
return BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listener: _onNotificationAction,
|
||||
child: _buildEditorPage(
|
||||
context,
|
||||
state,
|
||||
),
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
child: _buildEditorPage(context, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -107,6 +108,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
),
|
||||
header: _buildCoverAndIcon(context, state.editorState!),
|
||||
initialSelection: widget.initialSelection,
|
||||
);
|
||||
|
||||
return Column(
|
||||
@ -167,14 +169,12 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
NotificationActionState state,
|
||||
) async {
|
||||
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;
|
||||
if (editorState != null && widget.view.id == state.action?.objectId) {
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(
|
||||
Position(path: [path]),
|
||||
),
|
||||
Selection.collapsed(Position(path: [path])),
|
||||
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/presentation/editor_configuration.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/inline_actions_command.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/shortcuts/settings_shortcuts_service.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:flowy_infra/theme_extension.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';
|
||||
|
||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
@ -52,6 +50,7 @@ class AppFlowyEditorPage extends StatefulWidget {
|
||||
required this.styleCustomizer,
|
||||
this.showParagraphPlaceholder,
|
||||
this.placeholderText,
|
||||
this.initialSelection,
|
||||
});
|
||||
|
||||
final Widget? header;
|
||||
@ -63,6 +62,10 @@ class AppFlowyEditorPage extends StatefulWidget {
|
||||
final ShowPlaceholder? showParagraphPlaceholder;
|
||||
final String Function(Node)? placeholderText;
|
||||
|
||||
/// Used to provide an initial selection on Page-load
|
||||
///
|
||||
final Selection? initialSelection;
|
||||
|
||||
@override
|
||||
State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
|
||||
}
|
||||
@ -97,13 +100,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
...headingItems
|
||||
..forEach(
|
||||
(e) => e.isActive = onlyShowInSingleSelectionAndTextType,
|
||||
),
|
||||
...markdownFormatItems
|
||||
..forEach(
|
||||
(e) => e.isActive = showInAnyTextType,
|
||||
),
|
||||
..forEach((e) => e.isActive = onlyShowInSingleSelectionAndTextType),
|
||||
...markdownFormatItems..forEach((e) => e.isActive = showInAnyTextType),
|
||||
quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
bulletedListItem
|
||||
..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
|
||||
@ -177,14 +175,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
late final EditorScrollController editorScrollController;
|
||||
|
||||
Future<bool> showSlashMenu(editorState) async {
|
||||
final result = await customSlashCommand(
|
||||
slashMenuItems,
|
||||
shouldInsertSlash: false,
|
||||
style: styleCustomizer.selectionMenuStyleBuilder(),
|
||||
).handler(editorState);
|
||||
return result;
|
||||
}
|
||||
Future<bool> showSlashMenu(editorState) async => await customSlashCommand(
|
||||
slashMenuItems,
|
||||
shouldInsertSlash: false,
|
||||
style: styleCustomizer.selectionMenuStyleBuilder(),
|
||||
).handler(editorState);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -216,6 +211,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
// customize the dynamic theme color
|
||||
_customizeBlockComponentBackgroundColorDecorator();
|
||||
|
||||
if (widget.initialSelection != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.editorState.updateSelectionWithReason(
|
||||
widget.initialSelection,
|
||||
reason: SelectionUpdateReason.transaction,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -275,7 +279,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
);
|
||||
|
||||
final editorState = widget.editorState;
|
||||
_setInitialSelection(editorScrollController);
|
||||
|
||||
if (PlatformExtension.isMobile) {
|
||||
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() {
|
||||
final items = [...standardSelectionMenuItems];
|
||||
final imageItem = items.firstWhereOrNull(
|
||||
@ -387,9 +375,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
if (widget.editorState.document.isEmpty) {
|
||||
return (
|
||||
true,
|
||||
Selection.collapsed(
|
||||
Position(path: [0], offset: 0),
|
||||
),
|
||||
Selection.collapsed(Position(path: [0], offset: 0)),
|
||||
);
|
||||
}
|
||||
final nodes = widget.editorState.document.root.children
|
||||
@ -399,9 +385,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
if (isAllEmpty) {
|
||||
return (
|
||||
true,
|
||||
Selection.collapsed(
|
||||
Position(path: nodes.first.path, offset: 0),
|
||||
)
|
||||
Selection.collapsed(Position(path: nodes.first.path, offset: 0))
|
||||
);
|
||||
}
|
||||
return const (false, null);
|
||||
@ -421,9 +405,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
void _setRTLToolbarItems(bool isRTL) {
|
||||
final textDirectionItemIds = textDirectionItems.map((e) => e.id);
|
||||
// clear all the text direction items
|
||||
toolbarItems.removeWhere(
|
||||
(item) => textDirectionItemIds.contains(item.id),
|
||||
);
|
||||
toolbarItems.removeWhere((item) => textDirectionItemIds.contains(item.id));
|
||||
// only show the rtl item when the layout direction is ltr.
|
||||
if (isRTL) {
|
||||
toolbarItems.addAll(textDirectionItems);
|
||||
@ -441,20 +423,19 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
style,
|
||||
showReplaceMenu,
|
||||
onDismiss,
|
||||
) {
|
||||
return Material(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FindAndReplaceMenuWidget(
|
||||
editorState: editorState,
|
||||
onDismiss: onDismiss,
|
||||
),
|
||||
) =>
|
||||
Material(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FindAndReplaceMenuWidget(
|
||||
editorState: editorState,
|
||||
onDismiss: onDismiss,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -468,6 +449,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
if (tintColor != null) {
|
||||
return tintColor.color(context);
|
||||
}
|
||||
|
||||
final themeColor = themeBackgroundColors[colorString];
|
||||
if (themeColor != null) {
|
||||
return themeColor.color(context);
|
||||
@ -488,9 +470,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
};
|
||||
}
|
||||
|
||||
void _initEditorL10n() {
|
||||
AppFlowyEditorL10n.current = EditorI18n();
|
||||
}
|
||||
void _initEditorL10n() => AppFlowyEditorL10n.current = EditorI18n();
|
||||
|
||||
Future<void> _focusOnLastEmptyParagraph() async {
|
||||
final editorState = widget.editorState;
|
||||
@ -518,6 +498,7 @@ bool showInAnyTextType(EditorState editorState) {
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
return nodes.any(
|
||||
(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_page_block.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
enum MentionType {
|
||||
page,
|
||||
date,
|
||||
reminder;
|
||||
reminder,
|
||||
date;
|
||||
|
||||
static MentionType fromString(String value) {
|
||||
switch (value) {
|
||||
case 'page':
|
||||
return page;
|
||||
case 'date':
|
||||
return date;
|
||||
case 'reminder':
|
||||
return reminder;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
static MentionType fromString(String value) => switch (value) {
|
||||
'page' => page,
|
||||
'date' => date,
|
||||
// Backwards compatibility
|
||||
'reminder' => date,
|
||||
_ => 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 {
|
||||
const MentionBlockKeys._();
|
||||
|
||||
static const uid = 'uid'; // UniqueID
|
||||
static const reminderId = 'reminder_id'; // ReminderID
|
||||
static const mention = 'mention';
|
||||
static const type = 'type'; // MentionType, String
|
||||
static const pageId = 'page_id';
|
||||
|
||||
// Related to Reminder and Date blocks
|
||||
static const date = 'date';
|
||||
static const date = 'date'; // Start Date
|
||||
static const includeTime = 'include_time';
|
||||
static const reminderOption = 'reminder_option';
|
||||
}
|
||||
|
||||
class MentionBlock extends StatelessWidget {
|
||||
@ -62,21 +79,21 @@ class MentionBlock extends StatelessWidget {
|
||||
pageId: pageId,
|
||||
textStyle: textStyle,
|
||||
);
|
||||
case MentionType.reminder:
|
||||
case MentionType.date:
|
||||
final String date = mention[MentionBlockKeys.date];
|
||||
final BuildContext editorContext =
|
||||
context.read<EditorState>().document.root.context!;
|
||||
final editorState = context.read<EditorState>();
|
||||
final reminderOption = ReminderOption.values.firstWhereOrNull(
|
||||
(o) => o.name == mention[MentionBlockKeys.reminderOption],
|
||||
);
|
||||
|
||||
return MentionDateBlock(
|
||||
key: ValueKey(date),
|
||||
editorContext: editorContext,
|
||||
editorState: editorState,
|
||||
date: date,
|
||||
node: node,
|
||||
index: index,
|
||||
isReminder: type == MentionType.reminder,
|
||||
reminderId: type == MentionType.reminder
|
||||
? mention[MentionBlockKeys.uid]
|
||||
: null,
|
||||
reminderId: mention[MentionBlockKeys.reminderId],
|
||||
reminderOption: reminderOption,
|
||||
includeTime: mention[MentionBlockKeys.includeTime] ?? false,
|
||||
);
|
||||
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: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/more/cubit/document_appearance_cubit.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/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:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nanoid/non_secure.dart';
|
||||
|
||||
class MentionDateBlock extends StatefulWidget {
|
||||
const MentionDateBlock({
|
||||
super.key,
|
||||
required this.editorContext,
|
||||
required this.editorState,
|
||||
required this.date,
|
||||
required this.index,
|
||||
required this.node,
|
||||
this.isReminder = false,
|
||||
this.reminderId,
|
||||
this.reminderOption,
|
||||
this.includeTime = false,
|
||||
});
|
||||
|
||||
final BuildContext editorContext;
|
||||
final EditorState editorState;
|
||||
final String date;
|
||||
final int index;
|
||||
final Node node;
|
||||
|
||||
final bool isReminder;
|
||||
|
||||
/// If [isReminder] is true, then this must not be
|
||||
/// null or empty
|
||||
final String? reminderId;
|
||||
|
||||
final ReminderOption? reminderOption;
|
||||
|
||||
final bool includeTime;
|
||||
|
||||
@override
|
||||
@ -47,14 +59,13 @@ class MentionDateBlock extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
late bool includeTime = widget.includeTime;
|
||||
final PopoverMutex mutex = PopoverMutex();
|
||||
|
||||
late bool _includeTime = widget.includeTime;
|
||||
late DateTime? parsedDate = DateTime.tryParse(widget.date);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final editorState = context.read<EditorState>();
|
||||
|
||||
DateTime? parsedDate = DateTime.tryParse(widget.date);
|
||||
if (parsedDate == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@ -77,10 +88,9 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
builder: (context, state) {
|
||||
final reminder = state.reminders
|
||||
.firstWhereOrNull((r) => r.id == widget.reminderId);
|
||||
final noReminder = reminder == null && widget.isReminder;
|
||||
|
||||
final formattedDate = appearance.dateFormat
|
||||
.formatDate(parsedDate!, includeTime, appearance.timeFormat);
|
||||
.formatDate(parsedDate!, _includeTime, appearance.timeFormat);
|
||||
|
||||
final timeStr = parsedDate != null
|
||||
? _timeFromDate(parsedDate!, appearance.timeFormat)
|
||||
@ -90,28 +100,25 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
focusedDay: parsedDate,
|
||||
popoverMutex: mutex,
|
||||
selectedDay: parsedDate,
|
||||
firstDay: widget.isReminder
|
||||
? noReminder
|
||||
? parsedDate
|
||||
: DateTime.now()
|
||||
: null,
|
||||
lastDay: noReminder ? parsedDate : null,
|
||||
timeStr: timeStr,
|
||||
includeTime: includeTime,
|
||||
enableRanges: false,
|
||||
includeTime: _includeTime,
|
||||
dateFormat: appearance.dateFormat,
|
||||
timeFormat: appearance.timeFormat,
|
||||
enableRanges: true,
|
||||
selectedReminderOption: widget.reminderOption,
|
||||
onIncludeTimeChanged: (includeTime) {
|
||||
this.includeTime = includeTime;
|
||||
_updateBlock(parsedDate!.withoutTime, includeTime);
|
||||
_includeTime = includeTime;
|
||||
|
||||
// We can remove time from the date/reminder
|
||||
// block when toggled off.
|
||||
if (widget.isReminder) {
|
||||
_updateScheduledAt(
|
||||
reminderId: widget.reminderId!,
|
||||
selectedDay:
|
||||
includeTime ? parsedDate! : parsedDate!.withoutTime,
|
||||
if (![null, ReminderOption.none]
|
||||
.contains(widget.reminderOption)) {
|
||||
_updateReminder(
|
||||
widget.reminderOption!,
|
||||
reminder,
|
||||
includeTime,
|
||||
);
|
||||
} else {
|
||||
_updateBlock(
|
||||
parsedDate!.withoutTime,
|
||||
includeTime: includeTime,
|
||||
);
|
||||
}
|
||||
@ -121,37 +128,100 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
parsedDate = parsedDate!.withoutTime
|
||||
.add(Duration(hours: parsed.hour, minutes: parsed.minute));
|
||||
|
||||
_updateBlock(parsedDate!, includeTime);
|
||||
|
||||
if (widget.isReminder &&
|
||||
widget.date != parsedDate!.toIso8601String()) {
|
||||
_updateScheduledAt(
|
||||
reminderId: widget.reminderId!,
|
||||
selectedDay: parsedDate!,
|
||||
if (![null, ReminderOption.none]
|
||||
.contains(widget.reminderOption)) {
|
||||
_updateReminder(
|
||||
widget.reminderOption!,
|
||||
reminder,
|
||||
_includeTime,
|
||||
);
|
||||
} else {
|
||||
_updateBlock(parsedDate!, includeTime: _includeTime);
|
||||
}
|
||||
},
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
parsedDate = selectedDay;
|
||||
_updateBlock(selectedDay, includeTime);
|
||||
|
||||
if (widget.isReminder &&
|
||||
widget.date != selectedDay.toIso8601String()) {
|
||||
_updateScheduledAt(
|
||||
reminderId: widget.reminderId!,
|
||||
selectedDay: selectedDay,
|
||||
if (![null, ReminderOption.none]
|
||||
.contains(widget.reminderOption)) {
|
||||
_updateReminder(
|
||||
widget.reminderOption!,
|
||||
reminder,
|
||||
_includeTime,
|
||||
);
|
||||
} else {
|
||||
_updateBlock(selectedDay, includeTime: _includeTime);
|
||||
}
|
||||
},
|
||||
onReminderSelected: (reminderOption) =>
|
||||
_updateReminder(reminderOption, reminder),
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTapDown: editorState.editable
|
||||
? (details) => DatePickerMenu(
|
||||
context: context,
|
||||
editorState: context.read<EditorState>(),
|
||||
).show(details.globalPosition, options: options)
|
||||
: null,
|
||||
onTapDown: (details) {
|
||||
if (widget.editorState.editable) {
|
||||
if (PlatformExtension.isMobile) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
resizeToAvoidBottomInset: false,
|
||||
builder: (_) => DraggableScrollableSheet(
|
||||
expand: false,
|
||||
snap: true,
|
||||
initialChildSize: 0.7,
|
||||
minChildSize: 0.4,
|
||||
snapSizes: const [0.4, 0.7, 1.0],
|
||||
builder: (_, controller) => Material(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ListView(
|
||||
controller: controller,
|
||||
children: [
|
||||
ColoredBox(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: const Center(child: DragHandler()),
|
||||
),
|
||||
const MobileDateHeader(),
|
||||
MobileAppFlowyDatePicker(
|
||||
selectedDay: parsedDate,
|
||||
timeStr: timeStr,
|
||||
dateStr: parsedDate != null
|
||||
? options.dateFormat
|
||||
.formatDate(parsedDate!, _includeTime)
|
||||
: null,
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: MouseRegion(
|
||||
@ -160,11 +230,11 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowySvg(
|
||||
widget.isReminder
|
||||
widget.reminderId != null
|
||||
? FlowySvgs.clock_alarm_s
|
||||
: FlowySvgs.date_s,
|
||||
size: const Size.square(18.0),
|
||||
color: widget.isReminder && reminder?.isAck == true
|
||||
color: reminder?.isAck == true
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
),
|
||||
@ -172,7 +242,7 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
FlowyText(
|
||||
formattedDate,
|
||||
fontSize: fontSize,
|
||||
color: widget.isReminder && reminder?.isAck == true
|
||||
color: reminder?.isAck == true
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
),
|
||||
@ -191,11 +261,16 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
final twelveHourFormat = DateFormat('HH:mm a');
|
||||
final twentyFourHourFormat = DateFormat('HH:mm');
|
||||
|
||||
if (timeFormat == TimeFormatPB.TwelveHour) {
|
||||
return twelveHourFormat.parse(timeStr);
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
@ -210,43 +285,94 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
}
|
||||
|
||||
void _updateBlock(
|
||||
DateTime date, [
|
||||
DateTime date, {
|
||||
bool includeTime = false,
|
||||
]) {
|
||||
final editorState = widget.editorContext.read<EditorState>();
|
||||
final transaction = editorState.transaction
|
||||
String? reminderId,
|
||||
ReminderOption? reminderOption,
|
||||
}) {
|
||||
final rId = reminderId ??
|
||||
(reminderOption == ReminderOption.none ? null : widget.reminderId);
|
||||
|
||||
final transaction = widget.editorState.transaction
|
||||
..formatText(widget.node, widget.index, 1, {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: widget.isReminder
|
||||
? MentionType.reminder.name
|
||||
: MentionType.date.name,
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: date.toIso8601String(),
|
||||
MentionBlockKeys.uid: widget.reminderId,
|
||||
MentionBlockKeys.reminderId: rId,
|
||||
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
|
||||
// the cursor with the new block render
|
||||
editorState.updateSelectionWithReason(
|
||||
editorState.selection,
|
||||
widget.editorState.updateSelectionWithReason(
|
||||
widget.editorState.selection,
|
||||
reason: SelectionUpdateReason.transaction,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateScheduledAt({
|
||||
required String reminderId,
|
||||
required DateTime selectedDay,
|
||||
bool? includeTime,
|
||||
}) {
|
||||
widget.editorContext.read<ReminderBloc>().add(
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(
|
||||
void _updateReminder(
|
||||
ReminderOption reminderOption,
|
||||
ReminderPB? reminder, [
|
||||
bool includeTime = false,
|
||||
]) {
|
||||
final rootContext = widget.editorState.document.root.context;
|
||||
if (parsedDate == null || rootContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.reminderId != null) {
|
||||
_updateBlock(
|
||||
parsedDate!,
|
||||
includeTime: includeTime,
|
||||
reminderOption: reminderOption,
|
||||
);
|
||||
|
||||
if (ReminderOption.none == reminderOption && reminder != null) {
|
||||
// Delete existing reminder
|
||||
return rootContext
|
||||
.read<ReminderBloc>()
|
||||
.add(ReminderEvent.remove(reminderId: reminder.id));
|
||||
}
|
||||
|
||||
// Update existing reminder
|
||||
return rootContext.read<ReminderBloc>().add(
|
||||
ReminderEvent.update(
|
||||
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,
|
||||
scheduledAt: selectedDay,
|
||||
includeTime: includeTime,
|
||||
objectId: viewId,
|
||||
title: LocaleKeys.reminderNotification_title.tr(),
|
||||
message: LocaleKeys.reminderNotification_message.tr(),
|
||||
meta: {
|
||||
ReminderMetaKeys.includeTime: false.toString(),
|
||||
ReminderMetaKeys.blockId: widget.node.id,
|
||||
},
|
||||
scheduledAt: Int64(parsedDate!.millisecondsSinceEpoch ~/ 1000),
|
||||
isAck: parsedDate!.isBefore(DateTime.now()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,7 +1,10 @@
|
||||
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/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_v3/_toolbar_theme.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:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
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
|
||||
|
@ -252,9 +252,7 @@ class EditorStyleCustomizer {
|
||||
key: ValueKey(
|
||||
switch (type) {
|
||||
MentionType.page => mention[MentionBlockKeys.pageId],
|
||||
MentionType.date ||
|
||||
MentionType.reminder =>
|
||||
mention[MentionBlockKeys.date],
|
||||
MentionType.date => mention[MentionBlockKeys.date],
|
||||
_ => MentionBlockKeys.mention,
|
||||
},
|
||||
),
|
||||
|
@ -1,16 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/date/date_service.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.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/mention/mention_block.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_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_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
@ -147,9 +149,10 @@ class ReminderReferenceService {
|
||||
'\$',
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.reminder.name,
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
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(),
|
||||
message: LocaleKeys.reminderNotification_message.tr(),
|
||||
meta: {
|
||||
ReminderMetaKeys.includeTime.name: false.toString(),
|
||||
ReminderMetaKeys.blockId.name: node.id,
|
||||
ReminderMetaKeys.includeTime: false.toString(),
|
||||
ReminderMetaKeys.blockId: node.id,
|
||||
},
|
||||
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
|
||||
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/plugins/document/presentation/more/cubit/document_appearance_cubit.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/workspace/application/notifications/notification_action.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/settings/appearance/appearance_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/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@ -140,30 +144,33 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
|
||||
create: (_) => DocumentAppearanceCubit()..fetch(),
|
||||
),
|
||||
BlocProvider.value(value: getIt<NotificationActionBloc>()),
|
||||
BlocProvider.value(
|
||||
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
|
||||
),
|
||||
],
|
||||
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: (context, state) {
|
||||
if (state.action?.type == ActionType.openView) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final view =
|
||||
state.action!.arguments?[ActionArgumentKeys.view.name];
|
||||
final action = state.action;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (action?.type == ActionType.openView &&
|
||||
PlatformExtension.isDesktop) {
|
||||
final view = action!.arguments?[ActionArgumentKeys.view];
|
||||
if (view != null) {
|
||||
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>(
|
||||
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/card/card.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
|
||||
@ -489,10 +491,13 @@ GoRoute _mobileGridScreenRoute() {
|
||||
pageBuilder: (context, state) {
|
||||
final id = state.uri.queryParameters[MobileGridScreen.viewId]!;
|
||||
final title = state.uri.queryParameters[MobileGridScreen.viewTitle];
|
||||
final arguments = state.uri.queryParameters[MobileGridScreen.viewArgs];
|
||||
|
||||
return MaterialPage(
|
||||
child: MobileGridScreen(
|
||||
id: id,
|
||||
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_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_bloc.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:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'reminder_bloc.freezed.dart';
|
||||
|
||||
class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
||||
late final NotificationActionBloc actionBloc;
|
||||
late final ReminderService reminderService;
|
||||
late final NotificationActionBloc _actionBloc;
|
||||
late final ReminderService _reminderService;
|
||||
late final Timer timer;
|
||||
|
||||
ReminderBloc() : super(ReminderState()) {
|
||||
actionBloc = getIt<NotificationActionBloc>();
|
||||
reminderService = const ReminderService();
|
||||
_actionBloc = getIt<NotificationActionBloc>();
|
||||
_reminderService = const ReminderService();
|
||||
timer = _periodicCheck();
|
||||
|
||||
on<ReminderEvent>((event, emit) async {
|
||||
@ -42,7 +42,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
||||
reminders.remove(reminder);
|
||||
|
||||
reminder.isRead = true;
|
||||
await reminderService.updateReminder(reminder: reminder);
|
||||
await _reminderService.updateReminder(reminder: reminder);
|
||||
|
||||
updatedReminders.add(reminder);
|
||||
}
|
||||
@ -51,29 +51,29 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
||||
emit(state.copyWith(reminders: reminders));
|
||||
},
|
||||
started: () async {
|
||||
final remindersOrFailure = await reminderService.fetchReminders();
|
||||
final remindersOrFailure = await _reminderService.fetchReminders();
|
||||
|
||||
remindersOrFailure.fold(
|
||||
(error) => Log.error(error),
|
||||
(reminders) => emit(state.copyWith(reminders: reminders)),
|
||||
);
|
||||
},
|
||||
remove: (reminder) async {
|
||||
remove: (reminderId) async {
|
||||
final unitOrFailure =
|
||||
await reminderService.removeReminder(reminderId: reminder.id);
|
||||
await _reminderService.removeReminder(reminderId: reminderId);
|
||||
|
||||
unitOrFailure.fold(
|
||||
(error) => Log.error(error),
|
||||
(_) {
|
||||
final reminders = [...state.reminders];
|
||||
reminders.removeWhere((e) => e.id == reminder.id);
|
||||
reminders.removeWhere((e) => e.id == reminderId);
|
||||
emit(state.copyWith(reminders: reminders));
|
||||
},
|
||||
);
|
||||
},
|
||||
add: (reminder) async {
|
||||
final unitOrFailure =
|
||||
await reminderService.addReminder(reminder: reminder);
|
||||
await _reminderService.addReminder(reminder: reminder);
|
||||
|
||||
return unitOrFailure.fold(
|
||||
(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 {
|
||||
final reminder =
|
||||
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 failureOrUnit = await reminderService.updateReminder(
|
||||
final failureOrUnit = await _reminderService.updateReminder(
|
||||
reminder: updateObject.merge(a: reminder),
|
||||
);
|
||||
|
||||
@ -124,17 +137,34 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
||||
),
|
||||
);
|
||||
|
||||
actionBloc.add(
|
||||
NotificationActionEvent.performAction(
|
||||
action: NotificationAction(
|
||||
objectId: reminder.objectId,
|
||||
arguments: {
|
||||
ActionArgumentKeys.nodePath.name: path,
|
||||
ActionArgumentKeys.view.name: view,
|
||||
},
|
||||
),
|
||||
),
|
||||
String? rowId;
|
||||
if (view?.layout != ViewLayoutPB.Document) {
|
||||
rowId = reminder.meta[ReminderMetaKeys.rowId];
|
||||
}
|
||||
|
||||
final action = NotificationAction(
|
||||
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;
|
||||
}
|
||||
|
||||
final scheduledAt = DateTime.fromMillisecondsSinceEpoch(
|
||||
reminder.scheduledAt.toInt() * 1000,
|
||||
);
|
||||
final scheduledAt = reminder.scheduledAt.toDateTime();
|
||||
|
||||
if (scheduledAt.isBefore(now)) {
|
||||
final notificationSettings =
|
||||
@ -163,7 +191,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
||||
identifier: reminder.id,
|
||||
title: LocaleKeys.reminderNotification_title.tr(),
|
||||
body: LocaleKeys.reminderNotification_message.tr(),
|
||||
onClick: () => actionBloc.add(
|
||||
onClick: () => _actionBloc.add(
|
||||
NotificationActionEvent.performAction(
|
||||
action: NotificationAction(objectId: reminder.objectId),
|
||||
),
|
||||
@ -189,11 +217,19 @@ class ReminderEvent with _$ReminderEvent {
|
||||
const factory ReminderEvent.started() = _Started;
|
||||
|
||||
// Remove a reminder
|
||||
const factory ReminderEvent.remove({required ReminderPB reminder}) = _Remove;
|
||||
const factory ReminderEvent.remove({required String reminderId}) = _Remove;
|
||||
|
||||
// Add a reminder
|
||||
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.)
|
||||
const factory ReminderEvent.update(ReminderUpdate update) = _Update;
|
||||
|
||||
@ -232,7 +268,7 @@ class ReminderUpdate {
|
||||
|
||||
final meta = a.meta;
|
||||
if (includeTime != a.includeTime) {
|
||||
meta[ReminderMetaKeys.includeTime.name] = includeTime.toString();
|
||||
meta[ReminderMetaKeys.includeTime] = includeTime.toString();
|
||||
}
|
||||
|
||||
return ReminderPB(
|
||||
|
@ -1,17 +1,14 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
|
||||
enum ReminderMetaKeys {
|
||||
includeTime("include_time"),
|
||||
blockId("block_id");
|
||||
|
||||
const ReminderMetaKeys(this.name);
|
||||
|
||||
final String name;
|
||||
class ReminderMetaKeys {
|
||||
static String includeTime = "include_time";
|
||||
static String blockId = "block_id";
|
||||
static String rowId = "row_id";
|
||||
}
|
||||
|
||||
extension ReminderExtension on ReminderPB {
|
||||
bool? get includeTime {
|
||||
final String? includeTimeStr = meta[ReminderMetaKeys.includeTime.name];
|
||||
final String? includeTimeStr = meta[ReminderMetaKeys.includeTime];
|
||||
|
||||
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 {
|
||||
openView,
|
||||
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
|
||||
@ -31,12 +38,3 @@ class NotificationAction {
|
||||
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()) {
|
||||
on<NotificationActionEvent>((event, emit) async {
|
||||
event.when(
|
||||
performAction: (action) {
|
||||
emit(state.copyWith(action: action));
|
||||
performAction: (action, nextActions) {
|
||||
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 {
|
||||
const factory NotificationActionEvent.performAction({
|
||||
required NotificationAction action,
|
||||
@Default([]) List<NotificationAction> nextActions,
|
||||
}) = _PerformAction;
|
||||
}
|
||||
|
||||
class NotificationActionState {
|
||||
const NotificationActionState({required this.action});
|
||||
const NotificationActionState({
|
||||
required this.action,
|
||||
this.nextActions = const [],
|
||||
});
|
||||
|
||||
final NotificationAction? action;
|
||||
final List<NotificationAction> nextActions;
|
||||
|
||||
const NotificationActionState.initial() : action = null;
|
||||
const NotificationActionState.initial()
|
||||
: action = null,
|
||||
nextActions = const [];
|
||||
|
||||
NotificationActionState copyWith({
|
||||
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/startup/plugin/plugin.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_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'tabs_event.dart';
|
||||
@ -67,6 +68,14 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
|
||||
add(TabsEvent.openTab(plugin: view.plugin(), view: view));
|
||||
|
||||
/// Adds a [TabsEvent.openPlugin] event for the provided [ViewPB]
|
||||
void openPlugin(ViewPB view) =>
|
||||
add(TabsEvent.openPlugin(plugin: view.plugin(), view: view));
|
||||
void openPlugin(
|
||||
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/plugins/database/board/presentation/board_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_backend/protobuf/flowy-error/errors.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:flutter/material.dart';
|
||||
|
||||
enum FlowyPlugin {
|
||||
editor,
|
||||
kanban,
|
||||
}
|
||||
|
||||
class PluginArgumentKeys {
|
||||
static String selection = "selection";
|
||||
static String rowId = "row_id";
|
||||
}
|
||||
|
||||
extension ViewExtension on ViewPB {
|
||||
Widget defaultIcon() => FlowySvg(
|
||||
switch (layout) {
|
||||
@ -36,17 +43,30 @@ extension ViewExtension on ViewPB {
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
|
||||
Plugin plugin({bool listenOnViewChanged = false}) {
|
||||
Plugin plugin({
|
||||
bool listenOnViewChanged = false,
|
||||
Map<String, dynamic> arguments = const {},
|
||||
}) {
|
||||
switch (layout) {
|
||||
case ViewLayoutPB.Board:
|
||||
case ViewLayoutPB.Calendar:
|
||||
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:
|
||||
final Selection? initialSelection =
|
||||
arguments[PluginArgumentKeys.selection];
|
||||
|
||||
return DocumentPlugin(
|
||||
view: this,
|
||||
pluginType: pluginType,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
initialSelection: initialSelection,
|
||||
);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_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-user/protobuf.dart'
|
||||
show UserProfilePB;
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Home Sidebar is the left side bar of the home page.
|
||||
@ -63,6 +65,7 @@ class HomeSideBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: _onNotificationAction,
|
||||
),
|
||||
],
|
||||
@ -147,17 +150,21 @@ class HomeSideBar extends StatelessWidget {
|
||||
context.read<MenuBloc>().state.views.findView(action.objectId);
|
||||
|
||||
if (view != null) {
|
||||
context.read<TabsBloc>().openPlugin(view);
|
||||
final Map<String, dynamic> arguments = {};
|
||||
|
||||
final nodePath =
|
||||
action.arguments?[ActionArgumentKeys.nodePath.name] as int?;
|
||||
final nodePath = action.arguments?[ActionArgumentKeys.nodePath];
|
||||
if (nodePath != null) {
|
||||
context.read<NotificationActionBloc>().add(
|
||||
NotificationActionEvent.performAction(
|
||||
action: action.copyWith(type: ActionType.jumpToBlock),
|
||||
),
|
||||
);
|
||||
arguments[PluginArgumentKeys.selection] = Selection.collapsed(
|
||||
Position(path: [nodePath]),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
_reminderBloc.add(ReminderEvent.remove(reminder: reminder));
|
||||
_reminderBloc.add(ReminderEvent.remove(reminderId: reminder.id));
|
||||
}
|
||||
|
||||
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/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_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/notifications_hub_empty.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-user/reminder.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Displays a Lsit of Notifications, currently used primarily to
|
||||
/// display Reminders.
|
||||
@ -62,8 +63,7 @@ class NotificationsView extends StatelessWidget {
|
||||
children: [
|
||||
...shownReminders.map(
|
||||
(ReminderPB reminder) {
|
||||
final blockId =
|
||||
reminder.meta[ReminderMetaKeys.blockId.name];
|
||||
final blockId = reminder.meta[ReminderMetaKeys.blockId];
|
||||
|
||||
final documentService = DocumentService();
|
||||
final documentFuture = documentService.openDocument(
|
||||
@ -76,9 +76,7 @@ class NotificationsView extends StatelessWidget {
|
||||
_getNodeFromDocument(documentFuture, blockId);
|
||||
}
|
||||
|
||||
final view = views
|
||||
.firstWhereOrNull((v) => v.id == reminder.objectId);
|
||||
|
||||
final view = views.findView(reminder.objectId);
|
||||
return NotificationItem(
|
||||
reminderId: 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: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_type_option_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.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_editor/appflowy_editor.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 RangeSelectedCallback = Function(DateTime?, DateTime?, DateTime);
|
||||
@ -32,20 +38,27 @@ class AppFlowyDatePicker extends StatefulWidget {
|
||||
this.focusedDay,
|
||||
this.firstDay,
|
||||
this.lastDay,
|
||||
this.startDay,
|
||||
this.endDay,
|
||||
this.timeStr,
|
||||
this.endTimeStr,
|
||||
this.timeHintText,
|
||||
this.parseEndTimeError,
|
||||
this.parseTimeError,
|
||||
this.popoverMutex,
|
||||
this.selectedReminderOption = ReminderOption.none,
|
||||
this.onStartTimeSubmitted,
|
||||
this.onEndTimeSubmitted,
|
||||
this.onDaySelected,
|
||||
this.onRangeSelected,
|
||||
this.onReminderSelected,
|
||||
this.options,
|
||||
this.allowFormatChanges = false,
|
||||
this.onDateFormatChanged,
|
||||
this.onTimeFormatChanged,
|
||||
this.onClearDate,
|
||||
this.onCalendarCreated,
|
||||
this.onPageChanged,
|
||||
});
|
||||
|
||||
final bool includeTime;
|
||||
@ -64,17 +77,33 @@ class AppFlowyDatePicker extends StatefulWidget {
|
||||
final DateTime? focusedDay;
|
||||
final DateTime? firstDay;
|
||||
final DateTime? lastDay;
|
||||
|
||||
/// Start date in selected range
|
||||
final DateTime? startDay;
|
||||
|
||||
/// End date in selected range
|
||||
final DateTime? endDay;
|
||||
|
||||
final String? timeStr;
|
||||
final String? endTimeStr;
|
||||
final String? timeHintText;
|
||||
final String? parseEndTimeError;
|
||||
final String? parseTimeError;
|
||||
final PopoverMutex? popoverMutex;
|
||||
final ReminderOption selectedReminderOption;
|
||||
|
||||
final TimeChangedCallback? onStartTimeSubmitted;
|
||||
final TimeChangedCallback? onEndTimeSubmitted;
|
||||
final DaySelectedCallback? onDaySelected;
|
||||
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]
|
||||
/// cannot be null
|
||||
@ -94,21 +123,45 @@ class AppFlowyDatePicker extends StatefulWidget {
|
||||
///
|
||||
final VoidCallback? onClearDate;
|
||||
|
||||
final void Function(PageController pageController)? onCalendarCreated;
|
||||
|
||||
final void Function(DateTime focusedDay)? onPageChanged;
|
||||
|
||||
@override
|
||||
State<AppFlowyDatePicker> createState() => _AppFlowyDatePickerState();
|
||||
}
|
||||
|
||||
class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
|
||||
late DateTime? _selectedDay = widget.selectedDay;
|
||||
late ReminderOption _selectedReminderOption = widget.selectedReminderOption;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_selectedDay = widget.selectedDay;
|
||||
super.didChangeDependencies();
|
||||
Widget build(BuildContext context) =>
|
||||
PlatformExtension.isMobile ? buildMobilePicker() : buildDesktopPicker();
|
||||
|
||||
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 build(BuildContext context) {
|
||||
Widget buildDesktopPicker() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 18.0, bottom: 12.0),
|
||||
child: Column(
|
||||
@ -142,9 +195,14 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
|
||||
}
|
||||
},
|
||||
onRangeSelected: widget.onRangeSelected,
|
||||
selectedDay: _selectedDay,
|
||||
selectedDay:
|
||||
widget.rebuildOnDaySelected ? _selectedDay : widget.selectedDay,
|
||||
firstDay: widget.firstDay,
|
||||
lastDay: widget.lastDay,
|
||||
startDay: widget.startDay,
|
||||
endDay: widget.endDay,
|
||||
onCalendarCreated: widget.onCalendarCreated,
|
||||
onPageChanged: widget.onPageChanged,
|
||||
),
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
if (widget.enableRanges && widget.onIsRangeChanged != null) ...[
|
||||
@ -161,28 +219,44 @@ class _AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
|
||||
onChanged: widget.onIncludeTimeChanged,
|
||||
),
|
||||
),
|
||||
if (widget.onClearDate != null ||
|
||||
(widget.allowFormatChanges &&
|
||||
widget.onDateFormatChanged != null &&
|
||||
widget.onTimeFormatChanged != null))
|
||||
// Only show if either of the options are below it
|
||||
const TypeOptionSeparator(spacing: 8.0),
|
||||
if (widget.allowFormatChanges &&
|
||||
widget.onDateFormatChanged != null &&
|
||||
widget.onTimeFormatChanged != null)
|
||||
DateTypeOptionButton(
|
||||
popoverMutex: widget.popoverMutex,
|
||||
dateFormat: widget.dateFormat,
|
||||
timeFormat: widget.timeFormat,
|
||||
onDateFormatChanged: widget.onDateFormatChanged!,
|
||||
onTimeFormatChanged: widget.onTimeFormatChanged!,
|
||||
const _GroupSeparator(),
|
||||
ReminderSelector(
|
||||
mutex: widget.popoverMutex,
|
||||
selectedOption: _selectedReminderOption,
|
||||
onOptionSelected: (option) {
|
||||
setState(() => _selectedReminderOption = option);
|
||||
widget.onReminderSelected?.call(option);
|
||||
},
|
||||
),
|
||||
if (widget.options?.isNotEmpty ?? false) ...[
|
||||
const _GroupSeparator(),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.options!.length,
|
||||
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:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
@ -20,6 +21,8 @@ class DatePicker extends StatefulWidget {
|
||||
this.lastDay,
|
||||
this.onDaySelected,
|
||||
this.onRangeSelected,
|
||||
this.onCalendarCreated,
|
||||
this.onPageChanged,
|
||||
});
|
||||
|
||||
final bool isRange;
|
||||
@ -48,12 +51,16 @@ class DatePicker extends StatefulWidget {
|
||||
DateTime focusedDay,
|
||||
)? onRangeSelected;
|
||||
|
||||
final void Function(PageController pageController)? onCalendarCreated;
|
||||
|
||||
final void Function(DateTime focusedDay)? onPageChanged;
|
||||
|
||||
@override
|
||||
State<DatePicker> createState() => _DatePickerState();
|
||||
}
|
||||
|
||||
class _DatePickerState extends State<DatePicker> {
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
late DateTime _focusedDay = widget.selectedDay ?? DateTime.now();
|
||||
late CalendarFormat _calendarFormat = widget.calendarFormat;
|
||||
|
||||
@override
|
||||
@ -64,57 +71,56 @@ class _DatePickerState extends State<DatePicker> {
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TableCalendar(
|
||||
firstDay: widget.firstDay ?? kFirstDay,
|
||||
lastDay: widget.lastDay ?? kLastDay,
|
||||
focusedDay: _focusedDay,
|
||||
rowHeight: 26.0 + 7.0,
|
||||
rowHeight: calendarStyle.rowHeight,
|
||||
calendarFormat: _calendarFormat,
|
||||
availableCalendarFormats: const {CalendarFormat.month: 'Month'},
|
||||
daysOfWeekHeight: 17.0 + 8.0,
|
||||
daysOfWeekHeight: calendarStyle.dowHeight,
|
||||
rangeSelectionMode: widget.isRange
|
||||
? RangeSelectionMode.enforced
|
||||
: RangeSelectionMode.disabled,
|
||||
rangeStartDay: widget.isRange ? widget.startDay : null,
|
||||
rangeEndDay: widget.isRange ? widget.endDay : null,
|
||||
headerStyle: HeaderStyle(
|
||||
formatButtonVisible: false,
|
||||
titleCentered: true,
|
||||
titleTextStyle: textStyle,
|
||||
leftChevronMargin: EdgeInsets.zero,
|
||||
leftChevronPadding: EdgeInsets.zero,
|
||||
leftChevronIcon: FlowySvg(
|
||||
FlowySvgs.arrow_left_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
rightChevronPadding: EdgeInsets.zero,
|
||||
rightChevronMargin: EdgeInsets.zero,
|
||||
rightChevronIcon: FlowySvg(
|
||||
FlowySvgs.arrow_right_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
headerMargin: EdgeInsets.zero,
|
||||
headerPadding: const EdgeInsets.only(bottom: 8.0),
|
||||
),
|
||||
availableGestures: calendarStyle.availableGestures,
|
||||
availableCalendarFormats: const {CalendarFormat.month: 'Month'},
|
||||
onCalendarCreated: widget.onCalendarCreated,
|
||||
headerVisible: calendarStyle.headerVisible,
|
||||
headerStyle: calendarStyle.headerStyle,
|
||||
calendarStyle: CalendarStyle(
|
||||
cellMargin: const EdgeInsets.all(3.5),
|
||||
defaultDecoration: boxDecoration,
|
||||
selectedDecoration: boxDecoration.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: calendarStyle.selectedColor,
|
||||
),
|
||||
todayDecoration: boxDecoration.copyWith(
|
||||
color: Colors.transparent,
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary),
|
||||
border: Border.all(color: calendarStyle.selectedColor),
|
||||
),
|
||||
weekendDecoration: boxDecoration,
|
||||
outsideDecoration: boxDecoration,
|
||||
rangeStartDecoration: boxDecoration.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: calendarStyle.selectedColor,
|
||||
),
|
||||
rangeEndDecoration: boxDecoration.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: calendarStyle.selectedColor,
|
||||
),
|
||||
defaultTextStyle: textStyle,
|
||||
weekendTextStyle: textStyle,
|
||||
@ -140,10 +146,7 @@ class _DatePickerState extends State<DatePicker> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: AFThemeExtension.of(context).caption,
|
||||
),
|
||||
child: Text(label, style: calendarStyle.dowTextStyle),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -152,10 +155,71 @@ class _DatePickerState extends State<DatePicker> {
|
||||
widget.isRange ? false : isSameDay(widget.selectedDay, day),
|
||||
onFormatChanged: (calendarFormat) =>
|
||||
setState(() => _calendarFormat = calendarFormat),
|
||||
onPageChanged: (focusedDay) => setState(() => _focusedDay = focusedDay),
|
||||
onPageChanged: (focusedDay) {
|
||||
widget.onPageChanged?.call(focusedDay);
|
||||
setState(() => _focusedDay = focusedDay);
|
||||
},
|
||||
onDaySelected: widget.onDaySelected,
|
||||
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/services.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/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_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
|
||||
/// Provides arguemnts for [AppFlowyDatePicker] when showing
|
||||
@ -21,15 +21,20 @@ class DatePickerOptions {
|
||||
this.firstDay,
|
||||
this.lastDay,
|
||||
this.timeStr,
|
||||
this.endTimeStr,
|
||||
this.includeTime = false,
|
||||
this.isRange = false,
|
||||
this.enableRanges = true,
|
||||
this.dateFormat = UserDateFormatPB.Friendly,
|
||||
this.timeFormat = UserTimeFormatPB.TwentyFourHour,
|
||||
this.selectedReminderOption,
|
||||
this.onDaySelected,
|
||||
this.onIncludeTimeChanged,
|
||||
required this.onIncludeTimeChanged,
|
||||
this.onStartTimeChanged,
|
||||
this.onEndTimeChanged,
|
||||
this.onRangeSelected,
|
||||
this.onIsRangeChanged,
|
||||
this.onReminderSelected,
|
||||
}) : focusedDay = focusedDay ?? DateTime.now();
|
||||
|
||||
final DateTime focusedDay;
|
||||
@ -38,33 +43,35 @@ class DatePickerOptions {
|
||||
final DateTime? firstDay;
|
||||
final DateTime? lastDay;
|
||||
final String? timeStr;
|
||||
final String? endTimeStr;
|
||||
final bool includeTime;
|
||||
final bool isRange;
|
||||
final bool enableRanges;
|
||||
final UserDateFormatPB dateFormat;
|
||||
final UserTimeFormatPB timeFormat;
|
||||
final ReminderOption? selectedReminderOption;
|
||||
|
||||
final DaySelectedCallback? onDaySelected;
|
||||
final IncludeTimeChangedCallback? onIncludeTimeChanged;
|
||||
final IncludeTimeChangedCallback onIncludeTimeChanged;
|
||||
final TimeChangedCallback? onStartTimeChanged;
|
||||
final TimeChangedCallback? onEndTimeChanged;
|
||||
final RangeSelectedCallback? onRangeSelected;
|
||||
final Function(bool)? onIsRangeChanged;
|
||||
final OnReminderSelected? onReminderSelected;
|
||||
}
|
||||
|
||||
abstract class DatePickerService {
|
||||
void show(Offset offset);
|
||||
void show(Offset offset, {required DatePickerOptions options});
|
||||
void dismiss();
|
||||
}
|
||||
|
||||
const double _datePickerWidth = 260;
|
||||
const double _datePickerHeight = 355;
|
||||
const double _includeTimeHeight = 40;
|
||||
const double _datePickerHeight = 370;
|
||||
const double _includeTimeHeight = 32;
|
||||
const double _ySpacing = 15;
|
||||
|
||||
class DatePickerMenu extends DatePickerService {
|
||||
DatePickerMenu({
|
||||
required this.context,
|
||||
required this.editorState,
|
||||
});
|
||||
DatePickerMenu({required this.context, required this.editorState});
|
||||
|
||||
final BuildContext context;
|
||||
final EditorState editorState;
|
||||
@ -78,16 +85,10 @@ class DatePickerMenu extends DatePickerService {
|
||||
}
|
||||
|
||||
@override
|
||||
void show(
|
||||
Offset offset, {
|
||||
DatePickerOptions? options,
|
||||
}) =>
|
||||
void show(Offset offset, {required DatePickerOptions options}) =>
|
||||
_show(offset, options: options);
|
||||
|
||||
void _show(
|
||||
Offset offset, {
|
||||
DatePickerOptions? options,
|
||||
}) {
|
||||
void _show(Offset offset, {required DatePickerOptions options}) {
|
||||
dismiss();
|
||||
|
||||
final editorSize = editorState.renderBox!.size;
|
||||
@ -112,37 +113,35 @@ class DatePickerMenu extends DatePickerService {
|
||||
}
|
||||
|
||||
_menuEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SizedBox(
|
||||
height: editorSize.height,
|
||||
width: editorSize.width,
|
||||
child: RawKeyboardListener(
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
onKey: (event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: dismiss,
|
||||
child: Stack(
|
||||
children: [
|
||||
_AnimatedDatePicker(
|
||||
offset: Offset(offsetX, offsetY),
|
||||
showBelow: showBelow,
|
||||
options: options,
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (_) => Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SizedBox(
|
||||
height: editorSize.height,
|
||||
width: editorSize.width,
|
||||
child: RawKeyboardListener(
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
onKey: (event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: dismiss,
|
||||
child: Stack(
|
||||
children: [
|
||||
_AnimatedDatePicker(
|
||||
offset: Offset(offsetX, offsetY),
|
||||
showBelow: showBelow,
|
||||
options: options,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_menuEntry!);
|
||||
@ -153,28 +152,28 @@ class _AnimatedDatePicker extends StatefulWidget {
|
||||
const _AnimatedDatePicker({
|
||||
required this.offset,
|
||||
required this.showBelow,
|
||||
this.options,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
final Offset offset;
|
||||
final bool showBelow;
|
||||
final DatePickerOptions? options;
|
||||
final DatePickerOptions options;
|
||||
|
||||
@override
|
||||
State<_AnimatedDatePicker> createState() => _AnimatedDatePickerState();
|
||||
}
|
||||
|
||||
class _AnimatedDatePickerState extends State<_AnimatedDatePicker> {
|
||||
late bool _includeTime = widget.options?.includeTime ?? false;
|
||||
late bool _includeTime = widget.options.includeTime;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double dy = widget.offset.dy;
|
||||
if (!widget.showBelow && _includeTime) {
|
||||
dy = dy - _includeTimeHeight;
|
||||
dy -= _includeTimeHeight;
|
||||
}
|
||||
|
||||
dy = dy + (widget.showBelow ? _ySpacing : -_ySpacing);
|
||||
dy += (widget.showBelow ? _ySpacing : -_ySpacing);
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@ -185,30 +184,31 @@ class _AnimatedDatePickerState extends State<_AnimatedDatePicker> {
|
||||
Theme.of(context).cardColor,
|
||||
Theme.of(context).colorScheme.shadow,
|
||||
),
|
||||
constraints: BoxConstraints.loose(
|
||||
const Size(_datePickerWidth, 465),
|
||||
),
|
||||
constraints: BoxConstraints.loose(const Size(_datePickerWidth, 465)),
|
||||
child: AppFlowyDatePicker(
|
||||
popoverMutex: widget.options?.popoverMutex,
|
||||
includeTime: _includeTime,
|
||||
enableRanges: widget.options?.enableRanges ?? false,
|
||||
isRange: widget.options?.isRange ?? false,
|
||||
onIsRangeChanged: (_) {},
|
||||
timeStr: widget.options?.timeStr,
|
||||
dateFormat:
|
||||
widget.options?.dateFormat.simplified ?? DateFormatPB.Friendly,
|
||||
timeFormat: widget.options?.timeFormat.simplified ??
|
||||
TimeFormatPB.TwentyFourHour,
|
||||
selectedDay: widget.options?.selectedDay,
|
||||
onIncludeTimeChanged: (includeTime) {
|
||||
widget.options?.onIncludeTimeChanged?.call(!includeTime);
|
||||
widget.options.onIncludeTimeChanged.call(!includeTime);
|
||||
setState(() => _includeTime = !includeTime);
|
||||
},
|
||||
onStartTimeSubmitted: widget.options?.onStartTimeChanged,
|
||||
onDaySelected: widget.options?.onDaySelected,
|
||||
focusedDay: widget.options?.focusedDay ?? DateTime.now(),
|
||||
firstDay: widget.options?.firstDay,
|
||||
lastDay: widget.options?.lastDay,
|
||||
enableRanges: widget.options.enableRanges,
|
||||
isRange: widget.options.isRange,
|
||||
onIsRangeChanged: widget.options.onIsRangeChanged,
|
||||
dateFormat: widget.options.dateFormat.simplified,
|
||||
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/workspace/presentation/widgets/date_picker/utils/layout.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../utils/layout.dart';
|
||||
|
||||
class DateTimeSetting extends StatefulWidget {
|
||||
const DateTimeSetting({
|
||||
@ -35,7 +36,7 @@ class _DateTimeSettingState extends State<DateTimeSetting> {
|
||||
mutex: timeSettingPopoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(8, 0),
|
||||
popupBuilder: (BuildContext context) => DateFormatList(
|
||||
popupBuilder: (_) => DateFormatList(
|
||||
selectedFormat: widget.dateFormat,
|
||||
onSelected: _onDateFormatChanged,
|
||||
),
|
||||
@ -48,7 +49,7 @@ class _DateTimeSettingState extends State<DateTimeSetting> {
|
||||
mutex: timeSettingPopoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(8, 0),
|
||||
popupBuilder: (BuildContext context) => TimeFormatList(
|
||||
popupBuilder: (_) => TimeFormatList(
|
||||
selectedFormat: widget.timeFormat,
|
||||
onSelected: _onTimeFormatChanged,
|
||||
),
|
||||
|
@ -32,7 +32,7 @@ class EndTextField extends StatelessWidget {
|
||||
child: TimeTextField(
|
||||
isEndTime: true,
|
||||
timeFormat: timeFormat,
|
||||
timeStr: endTimeStr,
|
||||
endTimeStr: endTimeStr,
|
||||
popoverMutex: popoverMutex,
|
||||
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() {
|
||||
super.initState();
|
||||
|
||||
if (widget.isEndTime) {
|
||||
_textController.text = widget.endTimeStr ?? "";
|
||||
} else {
|
||||
_textController.text = widget.timeStr ?? "";
|
||||
}
|
||||
_textController.text =
|
||||
(widget.isEndTime ? widget.endTimeStr : widget.timeStr) ?? "";
|
||||
|
||||
if (!widget.isEndTime && widget.timeStr != null) {
|
||||
text = widget.timeStr!;
|
||||
@ -89,6 +86,7 @@ class _TimeTextFieldState extends State<TimeTextField> {
|
||||
child: FlowyTextField(
|
||||
text: text,
|
||||
focusNode: _focusNode,
|
||||
autoFocus: false,
|
||||
controller: _textController,
|
||||
submitOnLeave: true,
|
||||
hintText: widget.timeHintText,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppFlowyPopover extends StatelessWidget {
|
||||
final Widget child;
|
||||
@ -58,12 +59,11 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
offset: offset,
|
||||
clickHandler: clickHandler,
|
||||
popupBuilder: (context) {
|
||||
final child = popupBuilder(context);
|
||||
return _PopoverContainer(
|
||||
constraints: constraints,
|
||||
margin: margin,
|
||||
decoration: decoration,
|
||||
child: child,
|
||||
child: popupBuilder(context),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
@ -72,18 +72,17 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _PopoverContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final BoxConstraints constraints;
|
||||
final EdgeInsets margin;
|
||||
final Decoration? decoration;
|
||||
|
||||
const _PopoverContainer({
|
||||
required this.child,
|
||||
required this.margin,
|
||||
required this.constraints,
|
||||
required this.decoration,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final BoxConstraints constraints;
|
||||
final EdgeInsets margin;
|
||||
final Decoration? decoration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -581,7 +581,8 @@
|
||||
"newProperty": "New property",
|
||||
"deleteFieldPromptMessage": "Are you sure? This property will be deleted",
|
||||
"newColumn": "New Column",
|
||||
"format": "Format"
|
||||
"format": "Format",
|
||||
"reminderOnDateTooltip": "This cell has a scheduled reminder"
|
||||
},
|
||||
"rowPage": {
|
||||
"newField": "Add a new field",
|
||||
@ -1025,7 +1026,21 @@
|
||||
"includeTime": "Include time",
|
||||
"isRange": "End date",
|
||||
"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": {
|
||||
"yesterday": "Yesterday",
|
||||
@ -1089,6 +1104,7 @@
|
||||
"highlight": "Highlight",
|
||||
"color": "Color",
|
||||
"image": "Image",
|
||||
"date": "Date",
|
||||
"italic": "Italic",
|
||||
"link": "Link",
|
||||
"numberedList": "Numbered List",
|
||||
|
@ -34,7 +34,7 @@ resolver = "2"
|
||||
[workspace.dependencies]
|
||||
lib-dispatch = { workspace = true, path = "lib-dispatch" }
|
||||
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-codegen = { workspace = true, path = "build-tool/flowy-codegen" }
|
||||
flowy-derive = { workspace = true, path = "build-tool/flowy-derive" }
|
||||
@ -73,7 +73,7 @@ futures = "0.3.29"
|
||||
tokio = "1.34.0"
|
||||
tokio-stream = "0.1.14"
|
||||
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"
|
||||
|
||||
[profile.dev]
|
||||
|
@ -32,6 +32,9 @@ pub struct DateCellDataPB {
|
||||
|
||||
#[pb(index = 8)]
|
||||
pub is_range: bool,
|
||||
|
||||
#[pb(index = 9)]
|
||||
pub reminder_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
@ -59,6 +62,9 @@ pub struct DateChangesetPB {
|
||||
|
||||
#[pb(index = 8, one_of)]
|
||||
pub clear_flag: Option<bool>,
|
||||
|
||||
#[pb(index = 9, one_of)]
|
||||
pub reminder_id: Option<String>,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Local = 0,
|
||||
US = 1,
|
||||
|
@ -586,7 +586,9 @@ pub(crate) async fn update_date_cell_handler(
|
||||
include_time: data.include_time,
|
||||
is_range: data.is_range,
|
||||
clear_flag: data.clear_flag,
|
||||
reminder_id: data.reminder_id,
|
||||
};
|
||||
|
||||
let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?;
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
|
@ -79,6 +79,8 @@ impl TypeOptionCellDataSerde for DateTypeOption {
|
||||
let end_timestamp = cell_data.end_timestamp;
|
||||
let (end_date, end_time) = self.formatted_date_time_from_timestamp(&end_timestamp);
|
||||
|
||||
let reminder_id = cell_data.reminder_id;
|
||||
|
||||
DateCellDataPB {
|
||||
date,
|
||||
time,
|
||||
@ -88,6 +90,7 @@ impl TypeOptionCellDataSerde for DateTypeOption {
|
||||
end_timestamp: end_timestamp.unwrap_or_default(),
|
||||
include_time,
|
||||
is_range,
|
||||
reminder_id,
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,18 +260,20 @@ impl CellDataChangeset for DateTypeOption {
|
||||
cell: Option<Cell>,
|
||||
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
||||
// old date cell data
|
||||
let (previous_timestamp, previous_end_timestamp, include_time, is_range) = match cell {
|
||||
Some(cell) => {
|
||||
let cell_data = DateCellData::from(&cell);
|
||||
(
|
||||
cell_data.timestamp,
|
||||
cell_data.end_timestamp,
|
||||
cell_data.include_time,
|
||||
cell_data.is_range,
|
||||
)
|
||||
},
|
||||
None => (None, None, false, false),
|
||||
};
|
||||
let (previous_timestamp, previous_end_timestamp, include_time, is_range, reminder_id) =
|
||||
match cell {
|
||||
Some(cell) => {
|
||||
let cell_data = DateCellData::from(&cell);
|
||||
(
|
||||
cell_data.timestamp,
|
||||
cell_data.end_timestamp,
|
||||
cell_data.include_time,
|
||||
cell_data.is_range,
|
||||
cell_data.reminder_id,
|
||||
)
|
||||
},
|
||||
None => (None, None, false, false, String::new()),
|
||||
};
|
||||
|
||||
if changeset.clear_flag == Some(true) {
|
||||
let cell_data = DateCellData {
|
||||
@ -276,6 +281,7 @@ impl CellDataChangeset for DateTypeOption {
|
||||
end_timestamp: None,
|
||||
include_time,
|
||||
is_range,
|
||||
reminder_id: String::new(),
|
||||
};
|
||||
|
||||
return Ok((Cell::from(&cell_data), cell_data));
|
||||
@ -284,6 +290,7 @@ impl CellDataChangeset for DateTypeOption {
|
||||
// update include_time and is_range if necessary
|
||||
let include_time = changeset.include_time.unwrap_or(include_time);
|
||||
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
|
||||
// a new timestamp is included in the changeset without an accompanying
|
||||
@ -323,6 +330,7 @@ impl CellDataChangeset for DateTypeOption {
|
||||
end_timestamp,
|
||||
include_time,
|
||||
is_range,
|
||||
reminder_id,
|
||||
};
|
||||
|
||||
Ok((Cell::from(&cell_data), cell_data))
|
||||
|
@ -25,6 +25,7 @@ pub struct DateCellChangeset {
|
||||
pub include_time: Option<bool>,
|
||||
pub is_range: Option<bool>,
|
||||
pub clear_flag: Option<bool>,
|
||||
pub reminder_id: Option<String>,
|
||||
}
|
||||
|
||||
impl FromCellChangeset for DateCellChangeset {
|
||||
@ -50,15 +51,17 @@ pub struct DateCellData {
|
||||
pub include_time: bool,
|
||||
#[serde(default)]
|
||||
pub is_range: bool,
|
||||
pub reminder_id: String,
|
||||
}
|
||||
|
||||
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 {
|
||||
timestamp: Some(timestamp),
|
||||
end_timestamp: None,
|
||||
include_time,
|
||||
is_range,
|
||||
reminder_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -79,11 +82,14 @@ impl From<&Cell> for DateCellData {
|
||||
.and_then(|data| data.parse::<i64>().ok());
|
||||
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 reminder_id = cell.get_str_value("reminder_id").unwrap_or_default();
|
||||
|
||||
Self {
|
||||
timestamp,
|
||||
end_timestamp,
|
||||
include_time,
|
||||
is_range,
|
||||
reminder_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,6 +101,7 @@ impl From<&DateCellDataPB> for DateCellData {
|
||||
end_timestamp: Some(data.end_timestamp),
|
||||
include_time: data.include_time,
|
||||
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_bool_value("include_time", cell_data.include_time)
|
||||
.insert_bool_value("is_range", cell_data.is_range)
|
||||
.insert_str_value("reminder_id", cell_data.reminder_id.to_owned())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@ -145,6 +153,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
|
||||
end_timestamp: None,
|
||||
include_time: 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 include_time: Option<bool> = None;
|
||||
let mut is_range: Option<bool> = None;
|
||||
let mut reminder_id: Option<String> = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
@ -178,18 +188,23 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
|
||||
"is_range" => {
|
||||
is_range = map.next_value()?;
|
||||
},
|
||||
"reminder_id" => {
|
||||
reminder_id = map.next_value()?;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
let include_time = include_time.unwrap_or_default();
|
||||
let is_range = is_range.unwrap_or_default();
|
||||
let reminder_id = reminder_id.unwrap_or_default();
|
||||
|
||||
Ok(DateCellData {
|
||||
timestamp,
|
||||
end_timestamp,
|
||||
include_time,
|
||||
is_range,
|
||||
reminder_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ mod tests {
|
||||
end_timestamp: None,
|
||||
include_time: true,
|
||||
is_range: false,
|
||||
reminder_id: String::new(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@ -41,6 +42,7 @@ mod tests {
|
||||
end_timestamp: Some(1648533809),
|
||||
include_time: true,
|
||||
is_range: false,
|
||||
reminder_id: String::new(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@ -53,6 +55,7 @@ mod tests {
|
||||
end_timestamp: Some(1648533809),
|
||||
include_time: true,
|
||||
is_range: true,
|
||||
reminder_id: String::new(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
@ -115,6 +115,7 @@ pub fn make_date_cell_string(timestamp: i64) -> String {
|
||||
include_time: Some(false),
|
||||
is_range: Some(false),
|
||||
clear_flag: None,
|
||||
reminder_id: Some(String::new()),
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user