Compare commits
31
Commits
chat_rag_only
...
0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627b8ab43c | ||
|
|
84d55b5022 | ||
|
|
b53e20f746 | ||
|
|
cd06161dea | ||
|
|
776f70a0c7 | ||
|
|
1d1647d58d | ||
|
|
b3f94be33f | ||
|
|
ace398537b | ||
|
|
430e21aec2 | ||
|
|
0c1eb7306a | ||
|
|
db11886e5f | ||
|
|
0d13336b32 | ||
|
|
60ad397105 | ||
|
|
fca3189c97 | ||
|
|
e63f767926 | ||
|
|
8826e479eb | ||
|
|
8e4fe3d559 | ||
|
|
e2ee11e48a | ||
|
|
bdc0fa1f2a | ||
|
|
3836545682 | ||
|
|
02eb0e0b83 | ||
|
|
38c2937f64 | ||
|
|
06d5bc734b | ||
|
|
a521541cb7 | ||
|
|
7d61252e6a | ||
|
|
cb2b9ac537 | ||
|
|
af31f8cf6f | ||
|
|
87c1245a3f | ||
|
|
49d9417cac | ||
|
|
1f29235538 | ||
|
|
fd1e36b21a |
No files matched your search
+35
-3
@@ -1,8 +1,40 @@
|
||||
# Release Notes
|
||||
## Version 0.7.9 - 25/12/2024
|
||||
### New Features
|
||||
|
||||
## Version 0.8.0 - 06/01/2025
|
||||
### Bug Fixes
|
||||
Fixed error displaying in the page style menu
|
||||
Fixed filter logic in the icon picker
|
||||
Fixed error displaying in the Favorite/Recent page
|
||||
Fixed the color picker displaying when tapping down
|
||||
Fixed icons not being supported in subpage blocks
|
||||
Fixed recent icon functionality in the space icon menu
|
||||
Fixed "Insert Below" not auto-scrolling the table
|
||||
Fixed a to-do item with an emoji automatically creating a soft break
|
||||
Fixed header row/column tap areas being too small
|
||||
Fixed simple table alignment not working for items that wrap
|
||||
Fixed web content reverting after removing the inline code format on desktop
|
||||
Fixed inability to make changes to a row or column in the table when opening a new tab
|
||||
Fixed changing the language to CKB-KU causing a gray screen on mobile
|
||||
|
||||
## Version 0.7.9 - 30/12/2024
|
||||
### New Features
|
||||
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
|
||||
- Create beautiful documents with 22 content types and markdown support
|
||||
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
|
||||
- Invite members to your workspace for seamless collaboration
|
||||
- Create multiple public/private spaces to better organize your content
|
||||
- Simple Table is now available on Mobile, designed specifically for mobile devices.
|
||||
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
|
||||
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
|
||||
- Use '/' to insert a content block into a table cell on Desktop
|
||||
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
|
||||
- Add messages to an editable document while chatting with AI side by side
|
||||
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
|
||||
- Drag a page from the sidebar into a document to easily mention the page without typing its title
|
||||
- Paste as plain text, a new option in the right-click paste menu
|
||||
### Bug Fixes
|
||||
- Fixed misalignment in numbered lists
|
||||
- Resolved several bugs in the emoji menu
|
||||
- Fixed a bug with checklist items
|
||||
|
||||
## Version 0.7.8 - 18/12/2024
|
||||
### New Features
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.7.9"
|
||||
APPFLOWY_VERSION = "0.8.0"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -53,7 +53,7 @@ android {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "io.appflowy.appflowy"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'data_migration/data_migration_test_runner.dart'
|
||||
as data_migration_test_runner;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'set_env.dart' as preset_af_cloud_env_test;
|
||||
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
|
||||
import 'sidebar/sidebar_rename_untitled_test.dart'
|
||||
as sidebar_rename_untitled_test;
|
||||
@@ -26,4 +27,5 @@ Future<void> main() async {
|
||||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/emoji.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('Change slide bar space icon', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
final firstIcon = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(SidebarSpaceHeader),
|
||||
onHover: () async {
|
||||
final moreOption = find.byType(SpaceMorePopup);
|
||||
await tester.tapButton(moreOption);
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsNothing);
|
||||
await tester.tapSvgButton(SpaceMoreActionType.changeIcon.leftIconSvg);
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final icons = find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.iconContent,
|
||||
);
|
||||
expect(icons, findsOneWidget);
|
||||
await tester.tapIcon(EmojiIconData.icon(firstIcon));
|
||||
|
||||
final spaceHeader = find.byType(SidebarSpaceHeader);
|
||||
final spaceIcon = find.descendant(
|
||||
of: spaceHeader,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.iconContent,
|
||||
),
|
||||
);
|
||||
expect(spaceIcon, findsOneWidget);
|
||||
});
|
||||
}
|
||||
+66
-6
@@ -5,6 +5,7 @@ import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -102,8 +103,7 @@ void main() {
|
||||
expect(memberCount, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('only display one menu item in the workspace menu',
|
||||
(tester) async {
|
||||
testWidgets('workspace menu popover behavior test', (tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
@@ -128,6 +128,8 @@ void main() {
|
||||
final workspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name == name,
|
||||
);
|
||||
|
||||
// the workspace menu shouldn't conflict with logout
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
@@ -136,15 +138,73 @@ void main() {
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
final logoutButton = find.byType(WorkspaceMoreButton);
|
||||
await tester.tapButton(logoutButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);
|
||||
expect(moreButton, findsNothing);
|
||||
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);
|
||||
expect(moreButton, findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more action button for the same workspace shouldn't do
|
||||
// anything
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
// click it again
|
||||
await tester.tapButton(moreButton);
|
||||
|
||||
// nothing should happen
|
||||
expect(
|
||||
find.text(LocaleKeys.button_rename.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more button of another workspace should close the menu
|
||||
// for this one
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final otherWorspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name != name,
|
||||
);
|
||||
final otherMoreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name != name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
otherWorspaceItem,
|
||||
onHover: () async {
|
||||
expect(otherMoreButton, findsOneWidget);
|
||||
await tester.tapButton(otherMoreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
expect(moreButton, findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+9
-1
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
@@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('calendar', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
|
||||
+10
-3
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid edit field test:', () {
|
||||
testWidgets('rename existing field', (tester) async {
|
||||
|
||||
+43
-1
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -11,6 +13,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
// Test cases for the Document SubPageBlock that needs to be covered:
|
||||
@@ -37,7 +40,14 @@ import '../../shared/util.dart';
|
||||
const _defaultPageName = "";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Document SubPageBlock tests', () {
|
||||
testWidgets('Insert a new SubPageBlock from Slash menu items',
|
||||
@@ -498,6 +508,38 @@ void main() {
|
||||
|
||||
expect(find.text('Parent'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('Displaying icon of subpage', (tester) async {
|
||||
const firstPage = 'FirstPage';
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: firstPage);
|
||||
final icon = await tester.loadIcon();
|
||||
|
||||
/// create subpage
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_subPage_name.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
|
||||
/// add icon
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.tapIcon(icon);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.openPage(firstPage);
|
||||
|
||||
/// check if there is a icon in document
|
||||
final iconWidget = find.byWidgetPredicate((w) {
|
||||
if (w is! RawEmojiIconWidget) return false;
|
||||
final iconData = w.emoji.emoji;
|
||||
return iconData == icon.emoji;
|
||||
});
|
||||
expect(iconWidget, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ void main() {
|
||||
|
||||
// open settings page to check the result
|
||||
await tester.tapButton(settingsButton);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// check the server type
|
||||
expect(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
@@ -27,21 +25,6 @@ void main() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
Future<EmojiIconData> loadIcon() async {
|
||||
await loadIconGroups();
|
||||
final groups = kIconGroups!;
|
||||
final firstGroup = groups.first;
|
||||
final firstIcon = firstGroup.icons.first;
|
||||
return EmojiIconData.icon(
|
||||
IconsData(
|
||||
firstGroup.name,
|
||||
firstIcon.content,
|
||||
firstIcon.name,
|
||||
builtInSpaceColors.first,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('Update page emoji in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -160,7 +143,7 @@ void main() {
|
||||
testWidgets('Update page icon in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await loadIcon();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
@@ -192,7 +175,7 @@ void main() {
|
||||
testWidgets('Update page icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await loadIcon();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
|
||||
+9
-1
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -12,7 +13,14 @@ import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Sidebar view item tests', () {
|
||||
testWidgets('Access view item context menu by right click', (tester) async {
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('document page style:', () {
|
||||
double getCurrentEditorFontSize() {
|
||||
@@ -114,5 +127,37 @@ void main() {
|
||||
);
|
||||
expect(builtInCover, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('page style icon', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
final createPageButton =
|
||||
find.byKey(BottomNavigationBarItemType.add.valueKey);
|
||||
await tester.tapButton(createPageButton);
|
||||
|
||||
/// toggle the preset button
|
||||
await tester.tapSvgButton(FlowySvgs.m_layout_s);
|
||||
|
||||
/// select document plugins emoji
|
||||
final pageStyleIcon = find.byType(PageStyleIcon);
|
||||
|
||||
/// there should be none of emoji
|
||||
final noneText = find.text(LocaleKeys.pageStyle_none.tr());
|
||||
expect(noneText, findsOneWidget);
|
||||
await tester.tapButton(pageStyleIcon);
|
||||
|
||||
/// select an emoji
|
||||
const emoji = '😄';
|
||||
await tester.tapEmoji(emoji);
|
||||
await tester.tapSvgButton(FlowySvgs.m_layout_s);
|
||||
expect(noneText, findsNothing);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: pageStyleIcon,
|
||||
matching: find.text(emoji),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -243,6 +245,53 @@ void main() {
|
||||
expect(table.isHeaderColumnEnabled, isTrue);
|
||||
expect(table.isHeaderRowEnabled, isTrue);
|
||||
|
||||
// disable header column
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
final toggleButton = find.descendant(
|
||||
of: find.byType(SimpleTableHeaderActionButton),
|
||||
matching: find.byType(CupertinoSwitch),
|
||||
);
|
||||
await tester.tapButton(toggleButton);
|
||||
}
|
||||
|
||||
// enable header row
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
final toggleButton = find.descendant(
|
||||
of: find.byType(SimpleTableHeaderActionButton),
|
||||
matching: find.byType(CupertinoSwitch),
|
||||
);
|
||||
await tester.tapButton(toggleButton);
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
expect(table.isHeaderColumnEnabled, isFalse);
|
||||
expect(table.isHeaderRowEnabled, isFalse);
|
||||
|
||||
// set to page width
|
||||
{
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
@@ -371,13 +420,22 @@ void main() {
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// clear content
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_clearContents
|
||||
.tr(),
|
||||
),
|
||||
final clearContents = find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_clearContents
|
||||
.tr(),
|
||||
);
|
||||
|
||||
// clear content
|
||||
final scrollable = find.descendant(
|
||||
of: find.byType(SimpleTableBottomSheet),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
await tester.scrollUntilVisible(
|
||||
clearContents,
|
||||
100,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
await tester.tapButton(clearContents);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the first cell is empty
|
||||
@@ -427,7 +485,7 @@ void main() {
|
||||
// open the plus menu and select the heading block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.editor_toggleHeading1ShortForm.tr(),
|
||||
LocaleKeys.editor_heading1.tr(),
|
||||
);
|
||||
|
||||
// check the heading block is inserted
|
||||
|
||||
@@ -175,6 +175,33 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapDown(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
bool pumpAndSettle = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
final location = getCenter(finder);
|
||||
final TestGesture gesture = await startGesture(
|
||||
location,
|
||||
pointer: pointer,
|
||||
buttons: buttons,
|
||||
kind: kind,
|
||||
);
|
||||
await gesture.cancel();
|
||||
await gesture.down(location);
|
||||
await gesture.cancel();
|
||||
if (pumpAndSettle) {
|
||||
await this.pumpAndSettle(
|
||||
Duration(milliseconds: milliseconds),
|
||||
EnginePhase.sendSemanticsUpdate,
|
||||
const Duration(seconds: 15),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapButtonWithName(
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
@@ -23,6 +24,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
@@ -898,6 +900,22 @@ extension CommonOperations on WidgetTester {
|
||||
await tapAt(Offset.zero);
|
||||
await pumpUntilNotFound(finder);
|
||||
}
|
||||
|
||||
/// load icon list and return the first one
|
||||
Future<EmojiIconData> loadIcon() async {
|
||||
await loadIconGroups();
|
||||
final groups = kIconGroups!;
|
||||
final firstGroup = groups.first;
|
||||
final firstIcon = firstGroup.icons.first;
|
||||
return EmojiIconData.icon(
|
||||
IconsData(
|
||||
firstGroup.name,
|
||||
firstIcon.content,
|
||||
firstIcon.name,
|
||||
builtInSpaceColors.first,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsFinder on CommonFinders {
|
||||
|
||||
@@ -31,10 +31,7 @@ extension EmojiTestExtension on WidgetTester {
|
||||
matching: find.text(PickerTabType.icon.tr),
|
||||
);
|
||||
expect(iconTab, findsOneWidget);
|
||||
expect(find.byType(FlowyIconPicker), findsNothing);
|
||||
await tap(iconTab);
|
||||
await pumpAndSettle();
|
||||
expect(find.byType(FlowyIconPicker), findsOneWidget);
|
||||
await tapButton(iconTab);
|
||||
final selectedSvg = find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
matching: find.byWidgetPredicate(
|
||||
@@ -42,6 +39,11 @@ extension EmojiTestExtension on WidgetTester {
|
||||
),
|
||||
);
|
||||
expect(find.byType(IconColorPicker), findsNothing);
|
||||
|
||||
/// test for tapping down, it should not display the ColorPicker unless tapping up
|
||||
await tapDown(selectedSvg);
|
||||
expect(find.byType(IconColorPicker), findsNothing);
|
||||
|
||||
await tapButton(selectedSvg);
|
||||
final colorPicker = find.byType(IconColorPicker);
|
||||
expect(colorPicker, findsOneWidget);
|
||||
|
||||
@@ -190,14 +190,14 @@ SPEC CHECKSUMS:
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
|
||||
@@ -208,4 +208,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -268,8 +268,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
EmojiIconWidget(
|
||||
if (icon != null && icon.value.isNotEmpty) ...[
|
||||
RawEmojiIconWidget(
|
||||
emoji: icon.toEmojiIconData(),
|
||||
emojiSize: 15,
|
||||
),
|
||||
|
||||
+13
-8
@@ -57,6 +57,7 @@ Future<T?> showMobileBottomSheet<T>(
|
||||
double maxChildSize = 0.8,
|
||||
double initialChildSize = 0.51,
|
||||
double bottomSheetPadding = 0,
|
||||
bool enablePadding = true,
|
||||
}) async {
|
||||
assert(
|
||||
showHeader ||
|
||||
@@ -172,14 +173,18 @@ Future<T?> showMobileBottomSheet<T>(
|
||||
}
|
||||
|
||||
// ----- content area -----
|
||||
// add content padding and extra bottom padding
|
||||
children.add(
|
||||
Padding(
|
||||
padding:
|
||||
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
if (enablePadding) {
|
||||
// add content padding and extra bottom padding
|
||||
children.add(
|
||||
Padding(
|
||||
padding:
|
||||
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(child);
|
||||
}
|
||||
// ----- content area -----
|
||||
|
||||
if (children.length == 1) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/built_in_svgs.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
@@ -234,6 +235,7 @@ class _UserIcon extends StatelessWidget {
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.pageTitle:
|
||||
LocaleKeys.titleBar_userIcon.tr(),
|
||||
MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name],
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
|
||||
+10
-8
@@ -178,16 +178,18 @@ class MobileViewPage extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
child: EmojiIconWidget(
|
||||
emoji: icon,
|
||||
emojiSize: 17.0,
|
||||
if (icon.isNotEmpty) ...[
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
child: EmojiIconWidget(
|
||||
emoji: icon,
|
||||
emojiSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (icon.isNotEmpty) const WidgetSpan(child: HSpace(2.0)),
|
||||
const WidgetSpan(child: HSpace(8.0)),
|
||||
],
|
||||
TextSpan(
|
||||
text: name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
|
||||
@@ -37,8 +37,13 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
||||
// Only user enable chat with file and the plugin is already running
|
||||
final supportChatWithFile = chatState.fileEnabled &&
|
||||
chatState.pluginState.state == RunningStatePB.Running;
|
||||
|
||||
final aiType = chatState.pluginState.state == RunningStatePB.Running
|
||||
? AIType.localAI
|
||||
: AIType.appflowyAI;
|
||||
emit(
|
||||
state.copyWith(
|
||||
aiType: aiType,
|
||||
supportChatWithFile: supportChatWithFile,
|
||||
chatState: chatState,
|
||||
),
|
||||
|
||||
@@ -272,15 +272,19 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
|
||||
// 3 mean message response from AI
|
||||
if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) {
|
||||
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||
answerStreamMessageId;
|
||||
temporaryMessageIDMap.putIfAbsent(
|
||||
pb.messageId.toString(),
|
||||
() => answerStreamMessageId,
|
||||
);
|
||||
answerStreamMessageId = '';
|
||||
}
|
||||
|
||||
// 1 mean message response from User
|
||||
if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) {
|
||||
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||
questionStreamMessageId;
|
||||
temporaryMessageIDMap.putIfAbsent(
|
||||
pb.messageId.toString(),
|
||||
() => questionStreamMessageId,
|
||||
);
|
||||
questionStreamMessageId = '';
|
||||
}
|
||||
|
||||
@@ -422,8 +426,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
(question) {
|
||||
if (!isClosed) {
|
||||
final streamAnswer = _createAnswerStreamMessage(
|
||||
answerStream!,
|
||||
question.messageId,
|
||||
stream: answerStream!,
|
||||
questionMessageId: question.messageId,
|
||||
fakeQuestionMessageId: questionStreamMessage.id,
|
||||
);
|
||||
|
||||
lastSentMessage = question;
|
||||
@@ -479,8 +484,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
(success) {
|
||||
if (!isClosed) {
|
||||
final streamAnswer = _createAnswerStreamMessage(
|
||||
answerStream!,
|
||||
answerMessageId - 1,
|
||||
stream: answerStream!,
|
||||
questionMessageId: answerMessageId - 1,
|
||||
).copyWith(id: answerMessageIdString);
|
||||
|
||||
add(ChatEvent.receiveMessage(streamAnswer));
|
||||
@@ -491,11 +496,14 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
}
|
||||
|
||||
Message _createAnswerStreamMessage(
|
||||
AnswerStream stream,
|
||||
Int64 questionMessageId,
|
||||
) {
|
||||
answerStreamMessageId = (questionMessageId + 1).toString();
|
||||
Message _createAnswerStreamMessage({
|
||||
required AnswerStream stream,
|
||||
required Int64 questionMessageId,
|
||||
String? fakeQuestionMessageId,
|
||||
}) {
|
||||
answerStreamMessageId = fakeQuestionMessageId == null
|
||||
? (questionMessageId + 1).toString()
|
||||
: "${fakeQuestionMessageId}_ans";
|
||||
|
||||
return TextMessage(
|
||||
id: answerStreamMessageId,
|
||||
|
||||
+3
-1
@@ -84,8 +84,10 @@ class ChatEditDocumentService {
|
||||
final nodes = [
|
||||
if (rootIsEmpty || !isLastLineEmpty) paragraphNode(),
|
||||
...messageDocument.root.children,
|
||||
paragraphNode(),
|
||||
];
|
||||
final insertPath = rootIsEmpty || listEquals(lastNodeOrNull.path, const [0])
|
||||
final insertPath = rootIsEmpty ||
|
||||
listEquals(lastNodeOrNull.path, const [0]) && isLastLineEmpty
|
||||
? const [0]
|
||||
: lastNodeOrNull.path.next;
|
||||
|
||||
|
||||
+40
-24
@@ -4,6 +4,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@@ -103,7 +104,7 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
ChatSettingsCubit() : super(ChatSettingsState.initial());
|
||||
|
||||
List<String> selectedSourceIds = [];
|
||||
ChatSource? source;
|
||||
List<ChatSource> sources = [];
|
||||
List<ChatSource> selectedSources = [];
|
||||
String filter = '';
|
||||
|
||||
@@ -111,25 +112,32 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
selectedSourceIds = [...newSelectedSourceIds];
|
||||
}
|
||||
|
||||
void refreshSources(ViewPB view) async {
|
||||
void refreshSources(List<ViewPB> spaceViews) async {
|
||||
filter = "";
|
||||
final newSource = await _recursiveBuild(view, null);
|
||||
|
||||
_restrictSelectionIfNecessary(newSource.children);
|
||||
final newSources = await Future.wait(
|
||||
spaceViews.map((view) => _recursiveBuild(view, null)),
|
||||
);
|
||||
for (final source in newSources) {
|
||||
_restrictSelectionIfNecessary(source.children);
|
||||
}
|
||||
|
||||
final selected = _buildSelectedSources(newSource).toList();
|
||||
|
||||
newSource.toggleIsExpanded();
|
||||
final selected = newSources
|
||||
.map((source) => _buildSelectedSources(source))
|
||||
.flattened
|
||||
.toList();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedSources: selected,
|
||||
visibleSources: [newSource],
|
||||
visibleSources: newSources,
|
||||
),
|
||||
);
|
||||
|
||||
source?.dispose();
|
||||
source = newSource.copy();
|
||||
sources
|
||||
..forEach((e) => e.dispose())
|
||||
..clear()
|
||||
..addAll(newSources.map((e) => e.copy()));
|
||||
|
||||
selectedSources
|
||||
..forEach((e) => e.dispose())
|
||||
@@ -204,12 +212,13 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
for (final source in state.visibleSources) {
|
||||
source.dispose();
|
||||
}
|
||||
if (source == null) {
|
||||
if (sources.isEmpty) {
|
||||
emit(ChatSettingsState.initial());
|
||||
} else {
|
||||
final selected =
|
||||
selectedSources.map(_buildSearchResults).nonNulls.toList();
|
||||
final visible = [_buildSearchResults(source!)].nonNulls.toList();
|
||||
final visible =
|
||||
sources.map(_buildSearchResults).nonNulls.nonNulls.toList();
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedSources: selected,
|
||||
@@ -301,25 +310,21 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
}
|
||||
|
||||
void updateSelectedStatus() {
|
||||
if (source == null) {
|
||||
if (sources.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_recursiveUpdateSelectedStatus(source!);
|
||||
_restrictSelectionIfNecessary(source!.children);
|
||||
for (final source in sources) {
|
||||
_recursiveUpdateSelectedStatus(source);
|
||||
}
|
||||
_restrictSelectionIfNecessary(sources);
|
||||
for (final visibleSource in state.visibleSources) {
|
||||
visibleSource.dispose();
|
||||
}
|
||||
final visible = [_buildSearchResults(source!)].nonNulls.toList();
|
||||
final visible = sources.map(_buildSearchResults).nonNulls.toList();
|
||||
|
||||
selectedSources
|
||||
..forEach((e) => e.dispose())
|
||||
..clear()
|
||||
..addAll(_buildSelectedSources(source!));
|
||||
emit(
|
||||
state.copyWith(
|
||||
visibleSources: visible,
|
||||
selectedSources:
|
||||
selectedSources.map(_buildSearchResults).nonNulls.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -360,13 +365,24 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
?.toggleIsExpanded();
|
||||
}
|
||||
} else {
|
||||
source?.findChildBySourceId(chatSource.view.id)?.toggleIsExpanded();
|
||||
for (final source in sources) {
|
||||
final child = source.findChildBySourceId(chatSource.view.id);
|
||||
if (child != null) {
|
||||
child.toggleIsExpanded();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
source?.dispose();
|
||||
for (final child in sources) {
|
||||
child.dispose();
|
||||
}
|
||||
for (final child in selectedSources) {
|
||||
child.dispose();
|
||||
}
|
||||
for (final child in state.selectedSources) {
|
||||
child.dispose();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer {
|
||||
DefaultAppearanceSettings.getDefaultSelectionColor(context),
|
||||
defaultTextDirection: appearance.defaultTextDirection,
|
||||
textStyleConfiguration: TextStyleConfiguration(
|
||||
lineHeight: 1.4,
|
||||
lineHeight: 20 / 14,
|
||||
applyHeightToFirstAscent: true,
|
||||
applyHeightToLastDescent: true,
|
||||
text: baseTextStyle(fontFamily).copyWith(
|
||||
|
||||
+6
@@ -506,6 +506,12 @@ class _PromptBottomActions extends StatelessWidget {
|
||||
margin: DesktopAIPromptSizes.actionBarPadding,
|
||||
child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(
|
||||
builder: (context, state) {
|
||||
if (state.chatState == null) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: _sendButton(),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
// predefinedFormatButton(),
|
||||
|
||||
+1
-1
@@ -312,7 +312,7 @@ class _LeadingActions extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).cardColor,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: PromptInputMobileSelectSourcesButton(
|
||||
onUpdateSelectedSources: onUpdateSelectedSources,
|
||||
),
|
||||
|
||||
+4
-8
@@ -69,9 +69,9 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
value: cubit,
|
||||
),
|
||||
],
|
||||
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
|
||||
selector: (state) => state.currentSpace,
|
||||
builder: (context, spaceView) {
|
||||
child: BlocSelector<SpaceBloc, SpaceState, List<ViewPB>>(
|
||||
selector: (state) => state.spaces,
|
||||
builder: (context, spaces) {
|
||||
return BlocListener<ChatBloc, ChatState>(
|
||||
listener: (context, state) {
|
||||
cubit
|
||||
@@ -97,11 +97,7 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
if (spaceView != null) {
|
||||
context
|
||||
.read<ChatSettingsCubit>()
|
||||
.refreshSources(spaceView);
|
||||
}
|
||||
context.read<ChatSettingsCubit>().refreshSources(spaces);
|
||||
await showMobileBottomSheet<void>(
|
||||
context,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
|
||||
+14
-11
@@ -74,9 +74,9 @@ class _PromptInputDesktopSelectSourcesButtonState
|
||||
value: cubit,
|
||||
),
|
||||
],
|
||||
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
|
||||
selector: (state) => state.currentSpace,
|
||||
builder: (context, spaceView) {
|
||||
child: BlocSelector<SpaceBloc, SpaceState, List<ViewPB>>(
|
||||
selector: (state) => state.spaces,
|
||||
builder: (context, spaces) {
|
||||
return BlocListener<ChatBloc, ChatState>(
|
||||
listener: (context, state) {
|
||||
cubit
|
||||
@@ -90,15 +90,11 @@ class _PromptInputDesktopSelectSourcesButtonState
|
||||
margin: EdgeInsets.zero,
|
||||
controller: popoverController,
|
||||
onOpen: () {
|
||||
if (spaceView != null) {
|
||||
context.read<ChatSettingsCubit>().refreshSources(spaceView);
|
||||
}
|
||||
context.read<ChatSettingsCubit>().refreshSources(spaces);
|
||||
},
|
||||
onClose: () {
|
||||
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
|
||||
if (spaceView != null) {
|
||||
context.read<ChatSettingsCubit>().refreshSources(spaceView);
|
||||
}
|
||||
context.read<ChatSettingsCubit>().refreshSources(spaces);
|
||||
},
|
||||
popupBuilder: (_) {
|
||||
return BlocProvider.value(
|
||||
@@ -395,12 +391,19 @@ class ChatSourceTreeItemInner extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyHover(
|
||||
cursor: isSelectedSection ? SystemMouseCursors.basic : null,
|
||||
style: HoverStyle(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: isSelectedSection
|
||||
? Colors.transparent
|
||||
: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => onSelected?.call(chatSource),
|
||||
onTap: () {
|
||||
if (!isSelectedSection) {
|
||||
onSelected?.call(chatSource);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(4.0),
|
||||
|
||||
+68
-8
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@@ -19,6 +20,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
@@ -243,7 +245,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
constraints: const BoxConstraints.tightFor(width: 300, height: 400),
|
||||
onClose: () {
|
||||
if (spaceView != null) {
|
||||
context.read<ChatSettingsCubit>().refreshSources(spaceView);
|
||||
context.read<ChatSettingsCubit>().refreshSources([spaceView]);
|
||||
}
|
||||
widget.onOverrideVisibility?.call(false);
|
||||
},
|
||||
@@ -272,12 +274,12 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
onPressed: () async {
|
||||
final documentId = getOpenedDocumentId();
|
||||
if (documentId != null) {
|
||||
await onAddToExistingPage(documentId);
|
||||
DocumentBloc.findOpen(documentId)?.forceReloadDocumentState();
|
||||
await onAddToExistingPage(context, documentId);
|
||||
await forceReloadAndUpdateSelection(documentId);
|
||||
} else {
|
||||
widget.onOverrideVisibility?.call(true);
|
||||
if (spaceView != null) {
|
||||
context.read<ChatSettingsCubit>().refreshSources(spaceView);
|
||||
context.read<ChatSettingsCubit>().refreshSources([spaceView]);
|
||||
}
|
||||
popoverController.show();
|
||||
}
|
||||
@@ -296,9 +298,8 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
},
|
||||
onAddToExistingPage: (documentId) async {
|
||||
popoverController.close();
|
||||
await onAddToExistingPage(documentId);
|
||||
final view =
|
||||
await ViewBackendService.getView(documentId).toNullable();
|
||||
final view = await onAddToExistingPage(context, documentId);
|
||||
|
||||
if (context.mounted) {
|
||||
openPageFromMessage(context, view);
|
||||
}
|
||||
@@ -307,12 +308,20 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onAddToExistingPage(String documentId) async {
|
||||
Future<ViewPB?> onAddToExistingPage(
|
||||
BuildContext context,
|
||||
String documentId,
|
||||
) async {
|
||||
await ChatEditDocumentService.addMessageToPage(
|
||||
documentId,
|
||||
widget.textMessage,
|
||||
);
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
final view = await ViewBackendService.getView(documentId).toNullable();
|
||||
if (context.mounted) {
|
||||
showSaveMessageSuccessToast(context, view);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
void addMessageToNewPage(BuildContext context) async {
|
||||
@@ -325,12 +334,63 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
chatView.parentViewId,
|
||||
[widget.textMessage],
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
showSaveMessageSuccessToast(context, newView);
|
||||
openPageFromMessage(context, newView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
showToastNotification(
|
||||
context,
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: LocaleKeys.chat_addToNewPageSuccessToast.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' ',
|
||||
),
|
||||
TextSpan(
|
||||
text: view.nameOrDefault,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFFFFFFFF),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> forceReloadAndUpdateSelection(String documentId) async {
|
||||
final bloc = DocumentBloc.findOpen(documentId);
|
||||
if (bloc == null) {
|
||||
return;
|
||||
}
|
||||
await bloc.forceReloadDocumentState();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final editorState = bloc.state.editorState;
|
||||
final lastNodePath = editorState?.getLastSelectable()?.$1.path;
|
||||
if (editorState == null || lastNodePath == null) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: lastNodePath)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? getOpenedDocumentId() {
|
||||
final pageManager = getIt<TabsBloc>().state.currentPageManager;
|
||||
if (!pageManager.showSecondaryPluginNotifier.value) {
|
||||
|
||||
+79
-23
@@ -1,11 +1,17 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:time/time.dart';
|
||||
|
||||
class AIMessageMetadata extends StatefulWidget {
|
||||
@@ -68,30 +74,44 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
|
||||
runSpacing: 4.0,
|
||||
children: widget.sources.map(
|
||||
(m) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 24,
|
||||
maxWidth: 240,
|
||||
),
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
useIntrinsicWidth: true,
|
||||
radius: BorderRadius.circular(8.0),
|
||||
text: FlowyText(
|
||||
m.name,
|
||||
fontSize: 12,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.icon_document_s,
|
||||
size: const Size.square(16),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
onTap: () {
|
||||
widget.onSelectedMetadata?.call(m);
|
||||
if (isURL(m.id)) {
|
||||
return _MetadataButton(
|
||||
name: m.id,
|
||||
onTap: () => widget.onSelectedMetadata?.call(m),
|
||||
);
|
||||
} else if (isUUID(m.id)) {
|
||||
return FutureBuilder<ViewPB?>(
|
||||
future: ViewBackendService.getView(m.id)
|
||||
.then((f) => f.toNullable()),
|
||||
builder: (context, snapshot) {
|
||||
final data = snapshot.data;
|
||||
if (!snapshot.hasData ||
|
||||
snapshot.connectionState != ConnectionState.done ||
|
||||
data == null) {
|
||||
return _MetadataButton(
|
||||
name: m.name,
|
||||
onTap: () => widget.onSelectedMetadata?.call(m),
|
||||
);
|
||||
}
|
||||
return BlocProvider(
|
||||
create: (_) => ViewBloc(view: data),
|
||||
child: BlocBuilder<ViewBloc, ViewState>(
|
||||
builder: (context, state) {
|
||||
return _MetadataButton(
|
||||
name: state.view.nameOrDefault,
|
||||
onTap: () => widget.onSelectedMetadata?.call(m),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
);
|
||||
} else {
|
||||
return _MetadataButton(
|
||||
name: m.name,
|
||||
onTap: () => widget.onSelectedMetadata?.call(m),
|
||||
);
|
||||
}
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
@@ -101,3 +121,39 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MetadataButton extends StatelessWidget {
|
||||
const _MetadataButton({
|
||||
this.name = "",
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 24,
|
||||
maxWidth: 240,
|
||||
),
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
useIntrinsicWidth: true,
|
||||
radius: BorderRadius.circular(8.0),
|
||||
text: FlowyText(
|
||||
name,
|
||||
fontSize: 12,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.icon_document_s,
|
||||
size: const Size.square(16),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -100,7 +100,7 @@ class ChatAIMessageWidget extends StatelessWidget {
|
||||
onSelectedMetadata: onSelectedMetadata,
|
||||
),
|
||||
if (state.sources.isNotEmpty && !isLastMessage)
|
||||
const VSpace(16.0),
|
||||
const VSpace(8.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class _ChatErrorMessageWidgetState extends State<ChatErrorMessageWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.warning_filled_s,
|
||||
FlowySvgs.toast_error_filled_s,
|
||||
blendMode: null,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
@@ -10,6 +13,11 @@ import 'package:universal_platform/universal_platform.dart';
|
||||
/// on mobile
|
||||
void openPageFromMessage(BuildContext context, ViewPB? view) {
|
||||
if (view == null) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
|
||||
@@ -13,6 +13,18 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
EmojiData? kCachedEmojiData;
|
||||
const _kRecentEmojiCategoryId = 'Recent';
|
||||
|
||||
class EmojiPickerResult {
|
||||
EmojiPickerResult({
|
||||
required this.emojiId,
|
||||
required this.emoji,
|
||||
this.isRandom = false,
|
||||
});
|
||||
|
||||
final String emojiId;
|
||||
final String emoji;
|
||||
final bool isRandom;
|
||||
}
|
||||
|
||||
class FlowyEmojiPicker extends StatefulWidget {
|
||||
const FlowyEmojiPicker({
|
||||
super.key,
|
||||
@@ -21,7 +33,7 @@ class FlowyEmojiPicker extends StatefulWidget {
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final EmojiSelectedCallback onEmojiSelected;
|
||||
final ValueChanged<EmojiPickerResult> onEmojiSelected;
|
||||
final int emojiPerLine;
|
||||
final bool ensureFocus;
|
||||
|
||||
@@ -70,7 +82,9 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
||||
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
|
||||
),
|
||||
onEmojiSelected: (id, emoji) {
|
||||
widget.onEmojiSelected.call(id, emoji);
|
||||
widget.onEmojiSelected.call(
|
||||
EmojiPickerResult(emojiId: id, emoji: emoji),
|
||||
);
|
||||
RecentIcons.putEmoji(id);
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
@@ -106,7 +120,12 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
||||
onSkinToneChanged: (value) {
|
||||
skinTone.value = value;
|
||||
},
|
||||
onRandomEmojiSelected: widget.onEmojiSelected,
|
||||
onRandomEmojiSelected: (id, emoji) {
|
||||
widget.onEmojiSelected.call(
|
||||
EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true),
|
||||
);
|
||||
RecentIcons.putEmoji(id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,14 +11,17 @@ class MobileEmojiPickerScreen extends StatelessWidget {
|
||||
const MobileEmojiPickerScreen({
|
||||
super.key,
|
||||
this.title,
|
||||
this.selectedType,
|
||||
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
|
||||
});
|
||||
|
||||
final PickerTabType? selectedType;
|
||||
final String? title;
|
||||
final List<PickerTabType> tabs;
|
||||
|
||||
static const routeName = '/emoji_picker';
|
||||
static const pageTitle = 'title';
|
||||
static const iconSelectedType = 'iconSelectedType';
|
||||
static const selectTabs = 'tabs';
|
||||
|
||||
@override
|
||||
@@ -30,8 +33,9 @@ class MobileEmojiPickerScreen extends StatelessWidget {
|
||||
body: SafeArea(
|
||||
child: FlowyIconEmojiPicker(
|
||||
tabs: tabs,
|
||||
initialType: selectedType,
|
||||
onSelectedEmoji: (r) {
|
||||
context.pop<EmojiIconData>(r);
|
||||
context.pop<EmojiIconData>(r.data);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+18
-8
@@ -5,7 +5,6 @@ import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
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/shared/flowy_error_page.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
@@ -42,6 +41,7 @@ class MobileGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
initialRowId: initialRowId,
|
||||
shrinkWrap: shrinkWrap,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,12 +68,14 @@ class MobileGridPage extends StatefulWidget {
|
||||
required this.databaseController,
|
||||
this.onDeleted,
|
||||
this.initialRowId,
|
||||
this.shrinkWrap = false,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final DatabaseController databaseController;
|
||||
final VoidCallback? onDeleted;
|
||||
final String? initialRowId;
|
||||
final bool shrinkWrap;
|
||||
|
||||
@override
|
||||
State<MobileGridPage> createState() => _MobileGridPageState();
|
||||
@@ -104,7 +106,10 @@ class _MobileGridPageState extends State<MobileGridPage> {
|
||||
finish: (result) {
|
||||
_openRow(context, widget.initialRowId, true);
|
||||
return result.successOrFail.fold(
|
||||
(_) => GridShortcuts(child: GridPageContent(view: widget.view)),
|
||||
(_) => GridPageContent(
|
||||
view: widget.view,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
),
|
||||
(err) => Center(
|
||||
child: AppFlowyErrorPage(
|
||||
error: err,
|
||||
@@ -145,9 +150,11 @@ class GridPageContent extends StatefulWidget {
|
||||
const GridPageContent({
|
||||
super.key,
|
||||
required this.view,
|
||||
this.shrinkWrap = false,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final bool shrinkWrap;
|
||||
|
||||
@override
|
||||
State<GridPageContent> createState() => _GridPageContentState();
|
||||
@@ -196,6 +203,7 @@ class _GridPageContentState extends State<GridPageContent> {
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_GridHeader(
|
||||
contentScrollController: contentScrollController,
|
||||
@@ -207,11 +215,12 @@ class _GridPageContentState extends State<GridPageContent> {
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: getGridFabs(context),
|
||||
),
|
||||
if (!widget.shrinkWrap)
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: getGridFabs(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -256,7 +265,7 @@ class _GridRows extends StatelessWidget {
|
||||
buildWhen: (previous, current) => previous.fields != current.fields,
|
||||
builder: (context, state) {
|
||||
final double contentWidth = getMobileGridContentWidth(state.fields);
|
||||
return Expanded(
|
||||
return Flexible(
|
||||
child: _WrapScrollView(
|
||||
scrollController: scrollController,
|
||||
contentWidth: contentWidth,
|
||||
@@ -305,6 +314,7 @@ class _GridRows extends StatelessWidget {
|
||||
return ReorderableListView.builder(
|
||||
scrollController: scrollController.verticalController,
|
||||
buildDefaultDragHandles: false,
|
||||
shrinkWrap: true,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.transparent,
|
||||
child: child,
|
||||
|
||||
+28
-30
@@ -32,40 +32,38 @@ class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.tasks.isNotEmpty &&
|
||||
widget.tasks.length <= widget.segmentLimit)
|
||||
...List<Widget>.generate(
|
||||
widget.tasks.length,
|
||||
(index) => Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(2)),
|
||||
color: index < numFinishedTasks
|
||||
? completedTaskColor
|
||||
: AFThemeExtension.of(context).progressBarBGColor,
|
||||
child: widget.tasks.isNotEmpty &&
|
||||
widget.tasks.length <= widget.segmentLimit
|
||||
? Row(
|
||||
children: [
|
||||
...List<Widget>.generate(
|
||||
widget.tasks.length,
|
||||
(index) => Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(2)),
|
||||
color: index < numFinishedTasks
|
||||
? completedTaskColor
|
||||
: AFThemeExtension.of(context)
|
||||
.progressBarBGColor,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
height: 4.0,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
height: 4.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: LinearPercentIndicator(
|
||||
lineHeight: 4.0,
|
||||
percent: widget.percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: completedTaskColor,
|
||||
backgroundColor:
|
||||
AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(2),
|
||||
),
|
||||
: LinearPercentIndicator(
|
||||
lineHeight: 4.0,
|
||||
percent: widget.percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: completedTaskColor,
|
||||
backgroundColor:
|
||||
AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 45,
|
||||
|
||||
+5
-2
@@ -1,3 +1,4 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@@ -8,7 +9,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/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import '../../cell/editable_cell_builder.dart';
|
||||
@@ -188,6 +188,9 @@ class CellAccessoryContainer extends StatelessWidget {
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Wrap(spacing: 6, children: children);
|
||||
return SeparatedRow(
|
||||
separatorBuilder: () => const HSpace(6),
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -244,9 +244,9 @@ class _RenameRowPopoverState extends State<RenameRowPopover> {
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 18),
|
||||
defaultIcon: const FlowySvg(FlowySvgs.document_s),
|
||||
onSubmitted: (emoji, _) {
|
||||
widget.onUpdateIcon(emoji);
|
||||
PopoverContainer.of(context).close();
|
||||
onSubmitted: (r, _) {
|
||||
widget.onUpdateIcon(r.data);
|
||||
if (!r.keepOpen) PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
const HSpace(6),
|
||||
|
||||
@@ -422,8 +422,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
);
|
||||
}
|
||||
|
||||
void forceReloadDocumentState() {
|
||||
_documentCollabAdapter.syncV3();
|
||||
Future<void> forceReloadDocumentState() {
|
||||
return _documentCollabAdapter.syncV3();
|
||||
}
|
||||
|
||||
// this is only used for debug mode
|
||||
|
||||
+5
-1
@@ -85,7 +85,11 @@ class DocumentCollaboratorsBloc
|
||||
final ids = <dynamic>{};
|
||||
final sorted = states.value.values.toList()
|
||||
..sort((a, b) => b.timestamp.compareTo(a.timestamp))
|
||||
..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId));
|
||||
// filter the duplicate users
|
||||
..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId))
|
||||
// only keep version 1 and metadata is not empty
|
||||
..retainWhere((e) => e.version == 1)
|
||||
..retainWhere((e) => e.metadata.isNotEmpty);
|
||||
for (final state in sorted) {
|
||||
if (state.version != 1) {
|
||||
continue;
|
||||
|
||||
+68
-23
@@ -7,20 +7,7 @@ import 'package:appflowy/plugins/document/application/document_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/ask_ai_block_component.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show
|
||||
EditorState,
|
||||
Transaction,
|
||||
Operation,
|
||||
InsertOperation,
|
||||
UpdateOperation,
|
||||
DeleteOperation,
|
||||
PathExtensions,
|
||||
Node,
|
||||
Path,
|
||||
Delta,
|
||||
composeAttributes,
|
||||
blockComponentDelta;
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
@@ -287,11 +274,6 @@ extension on UpdateOperation {
|
||||
// create the external text if the node contains the delta in its data.
|
||||
final prevDelta = oldAttributes[blockComponentDelta];
|
||||
final delta = attributes[blockComponentDelta];
|
||||
final diff = prevDelta != null && delta != null
|
||||
? Delta.fromJson(prevDelta).diff(
|
||||
Delta.fromJson(delta),
|
||||
)
|
||||
: null;
|
||||
|
||||
final composedAttributes = composeAttributes(oldAttributes, attributes);
|
||||
final composedDelta = composedAttributes?[blockComponentDelta];
|
||||
@@ -312,12 +294,15 @@ extension on UpdateOperation {
|
||||
// to be compatible with the old version, we create a new text id if the text id is empty.
|
||||
final textId = nanoid(6);
|
||||
final textDelta = composedDelta ?? delta ?? prevDelta;
|
||||
final textDeltaPayloadPB = textDelta == null
|
||||
final correctedTextDelta =
|
||||
textDelta != null ? _correctAttributes(textDelta) : null;
|
||||
|
||||
final textDeltaPayloadPB = correctedTextDelta == null
|
||||
? null
|
||||
: TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: jsonEncode(textDelta),
|
||||
delta: jsonEncode(correctedTextDelta),
|
||||
);
|
||||
|
||||
node.externalValues = ExternalValues(
|
||||
@@ -342,12 +327,20 @@ extension on UpdateOperation {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final textDeltaPayloadPB = delta == null
|
||||
final diff = prevDelta != null && delta != null
|
||||
? Delta.fromJson(prevDelta).diff(
|
||||
Delta.fromJson(delta),
|
||||
)
|
||||
: null;
|
||||
|
||||
final correctedDiff = diff != null ? _correctDelta(diff) : null;
|
||||
|
||||
final textDeltaPayloadPB = correctedDiff == null
|
||||
? null
|
||||
: TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: jsonEncode(diff),
|
||||
delta: jsonEncode(correctedDiff),
|
||||
);
|
||||
|
||||
if (enableDocumentInternalLog) {
|
||||
@@ -370,6 +363,58 @@ extension on UpdateOperation {
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
// if the value in Delta's attributes is false, we should set the value to null instead.
|
||||
// because on Yjs, canceling format must use the null value. If we use false, the update will be rejected.
|
||||
List<TextOperation>? _correctDelta(Delta delta) {
|
||||
// if the value in diff's attributes is false, we should set the value to null instead.
|
||||
// because on Yjs, canceling format must use the null value. If we use false, the update will be rejected.
|
||||
final correctedOps = delta.map((op) {
|
||||
final attributes = op.attributes?.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
// if the value is false, we should set the value to null instead.
|
||||
value == false ? null : value,
|
||||
),
|
||||
);
|
||||
|
||||
if (attributes != null) {
|
||||
if (op is TextRetain) {
|
||||
return TextRetain(op.length, attributes: attributes);
|
||||
} else if (op is TextInsert) {
|
||||
return TextInsert(op.text, attributes: attributes);
|
||||
}
|
||||
// ignore the other operations that do not contain attributes.
|
||||
}
|
||||
|
||||
return op;
|
||||
});
|
||||
|
||||
return correctedOps.toList(growable: false);
|
||||
}
|
||||
|
||||
// Refer to [_correctDelta] for more details.
|
||||
List<Map<String, dynamic>> _correctAttributes(
|
||||
List<Map<String, dynamic>> attributes,
|
||||
) {
|
||||
final correctedAttributes = attributes.map((attribute) {
|
||||
return attribute.map((key, value) {
|
||||
if (value is bool) {
|
||||
return MapEntry(key, value == false ? null : value);
|
||||
} else if (value is Map<String, dynamic>) {
|
||||
return MapEntry(
|
||||
key,
|
||||
value.map((key, value) {
|
||||
return MapEntry(key, value == false ? null : value);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return MapEntry(key, value);
|
||||
});
|
||||
}).toList(growable: false);
|
||||
|
||||
return correctedAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
extension on DeleteOperation {
|
||||
|
||||
+57
-14
@@ -399,10 +399,11 @@ ParagraphBlockComponentBuilder _buildParagraphBlockComponentBuilder(
|
||||
return ParagraphBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: placeholderText,
|
||||
textStyle: (node) => _buildTextStyleInTableCell(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
textAlign: (node) => _buildTextAlignInTableCell(
|
||||
context,
|
||||
@@ -421,10 +422,11 @@ TodoListBlockComponentBuilder _buildTodoListBlockComponentBuilder(
|
||||
return TodoListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(),
|
||||
textStyle: (node) => _buildTextStyleInTableCell(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
textAlign: (node) => _buildTextAlignInTableCell(
|
||||
context,
|
||||
@@ -451,10 +453,11 @@ BulletedListBlockComponentBuilder _buildBulletedListBlockComponentBuilder(
|
||||
return BulletedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(),
|
||||
textStyle: (node) => _buildTextStyleInTableCell(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
textAlign: (node) => _buildTextAlignInTableCell(
|
||||
context,
|
||||
@@ -473,10 +476,11 @@ NumberedListBlockComponentBuilder _buildNumberedListBlockComponentBuilder(
|
||||
return NumberedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(),
|
||||
textStyle: (node) => _buildTextStyleInTableCell(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
textAlign: (node) => _buildTextAlignInTableCell(
|
||||
context,
|
||||
@@ -507,10 +511,11 @@ QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder(
|
||||
return QuoteBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_quote.tr(),
|
||||
textStyle: (node) => _buildTextStyleInTableCell(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
textAlign: (node) => _buildTextAlignInTableCell(
|
||||
context,
|
||||
@@ -529,6 +534,12 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
|
||||
) {
|
||||
return HeadingBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
padding: (node) {
|
||||
if (customHeadingPadding != null) {
|
||||
return customHeadingPadding;
|
||||
@@ -567,7 +578,9 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
),
|
||||
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
||||
textStyleBuilder: (level) {
|
||||
return styleCustomizer.headingStyleBuilder(level);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -663,7 +676,12 @@ DatabaseViewBlockComponentBuilder _buildDatabaseViewBlockComponentBuilder(
|
||||
) {
|
||||
return DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return configuration.padding(node);
|
||||
}
|
||||
return const EdgeInsets.symmetric(vertical: 10);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -686,10 +704,11 @@ CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder(
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
),
|
||||
textStyle: (node) => _buildTextStyleInTableCell(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
),
|
||||
inlinePadding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
@@ -777,11 +796,12 @@ ToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder(
|
||||
|
||||
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
|
||||
},
|
||||
textStyle: (node) {
|
||||
textStyle: (node, {TextSpan? textSpan}) {
|
||||
final textStyle = _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
);
|
||||
final level = node.attributes[ToggleListBlockKeys.level] as int?;
|
||||
if (level == null) {
|
||||
@@ -816,9 +836,14 @@ OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder(
|
||||
) {
|
||||
return OutlineBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderTextStyle: (_) =>
|
||||
placeholderTextStyle: (node, {TextSpan? textSpan}) =>
|
||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return configuration.padding(node);
|
||||
}
|
||||
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -829,7 +854,12 @@ LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(
|
||||
) {
|
||||
return LinkPreviewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return configuration.padding(node);
|
||||
}
|
||||
return const EdgeInsets.symmetric(vertical: 10);
|
||||
},
|
||||
),
|
||||
cache: LinkPreviewDataCache(),
|
||||
showMenu: true,
|
||||
@@ -865,7 +895,14 @@ SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder(
|
||||
}) {
|
||||
return SubPageBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (node) => styleCustomizer.subPageBlockTextStyleBuilder(),
|
||||
textStyle: (node, {TextSpan? textSpan}) =>
|
||||
styleCustomizer.subPageBlockTextStyleBuilder(),
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return const EdgeInsets.symmetric(horizontal: 18);
|
||||
}
|
||||
return configuration.padding(node);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -874,8 +911,9 @@ TextStyle _buildTextStyleInTableCell(
|
||||
BuildContext context, {
|
||||
required Node node,
|
||||
required BlockComponentConfiguration configuration,
|
||||
required TextSpan? textSpan,
|
||||
}) {
|
||||
TextStyle textStyle = configuration.textStyle(node);
|
||||
TextStyle textStyle = configuration.textStyle(node, textSpan: textSpan);
|
||||
|
||||
if (node.isInHeaderColumn ||
|
||||
node.isInHeaderRow ||
|
||||
@@ -888,6 +926,11 @@ TextStyle _buildTextStyleInTableCell(
|
||||
|
||||
final cellTextColor = node.textColorInColumn ?? node.textColorInRow;
|
||||
|
||||
// enable it if we need to support the text color of the text span
|
||||
// final isTextSpanColorNull = textSpan?.style?.color == null;
|
||||
// final isTextSpanChildrenColorNull =
|
||||
// textSpan?.children?.every((e) => e.style?.color == null) ?? true;
|
||||
|
||||
if (cellTextColor != null) {
|
||||
textStyle = textStyle.copyWith(
|
||||
color: buildEditorCustomizedColor(
|
||||
|
||||
+14
-6
@@ -1,14 +1,14 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/patterns/file_type_patterns.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const _excludeFromDropTarget = [
|
||||
@@ -16,6 +16,9 @@ const _excludeFromDropTarget = [
|
||||
CustomImageBlockKeys.type,
|
||||
MultiImageBlockKeys.type,
|
||||
FileBlockKeys.type,
|
||||
SimpleTableBlockKeys.type,
|
||||
SimpleTableCellBlockKeys.type,
|
||||
SimpleTableRowBlockKeys.type,
|
||||
];
|
||||
|
||||
class EditorDropHandler extends StatelessWidget {
|
||||
@@ -38,8 +41,13 @@ class EditorDropHandler extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final childWidget = Consumer<EditorDropManagerState>(
|
||||
builder: (context, dropState, _) => DragTarget<ViewPB>(
|
||||
onLeave: (_) => editorState.selectionService.removeDropTarget(),
|
||||
onLeave: (_) {
|
||||
editorState.selectionService.removeDropTarget();
|
||||
disableAutoScrollWhenDragging = false;
|
||||
},
|
||||
onMove: (details) {
|
||||
disableAutoScrollWhenDragging = true;
|
||||
|
||||
if (details.data.id == viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -505,6 +505,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
Position(path: lastNode.path),
|
||||
);
|
||||
}
|
||||
|
||||
transaction.customSelectionType = SelectionType.inline;
|
||||
transaction.reason = SelectionUpdateReason.uiEvent;
|
||||
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -140,7 +140,9 @@ bool shouldIgnoreDragTarget({
|
||||
}
|
||||
|
||||
final targetNode = editorState.getNodeAtPath(targetPath);
|
||||
if (targetNode != null && targetNode.isInTable) {
|
||||
if (targetNode != null &&
|
||||
targetNode.isInTable &&
|
||||
targetNode.type != SimpleTableBlockKeys.type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ class VisualDragArea extends StatelessWidget {
|
||||
final DragAreaBuilderData data;
|
||||
final Node dragNode;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final targetNode = data.targetNode;
|
||||
|
||||
+3
-3
@@ -27,11 +27,11 @@ enum OptionAlignType {
|
||||
FlowySvgData get svg {
|
||||
switch (this) {
|
||||
case OptionAlignType.left:
|
||||
return FlowySvgs.align_left_s;
|
||||
return FlowySvgs.table_align_left_s;
|
||||
case OptionAlignType.center:
|
||||
return FlowySvgs.align_center_s;
|
||||
return FlowySvgs.table_align_center_s;
|
||||
case OptionAlignType.right:
|
||||
return FlowySvgs.align_right_s;
|
||||
return FlowySvgs.table_align_right_s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-8
@@ -26,8 +26,10 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final Size emojiPickerSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
final Widget? defaultIcon;
|
||||
final Offset? offset;
|
||||
@@ -85,8 +87,10 @@ class _DesktopEmojiPickerButton extends StatelessWidget {
|
||||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final Size emojiPickerSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
final Widget? defaultIcon;
|
||||
final Offset? offset;
|
||||
@@ -113,6 +117,7 @@ class _DesktopEmojiPickerButton extends StatelessWidget {
|
||||
height: emojiPickerSize.height,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: emoji.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
onSubmitted(r, popoverController);
|
||||
},
|
||||
@@ -156,8 +161,10 @@ class _MobileEmojiPickerButton extends StatelessWidget {
|
||||
|
||||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final String? title;
|
||||
final bool enable;
|
||||
final EdgeInsets? margin;
|
||||
@@ -177,11 +184,14 @@ class _MobileEmojiPickerButton extends StatelessWidget {
|
||||
final result = await context.push<EmojiIconData>(
|
||||
Uri(
|
||||
path: MobileEmojiPickerScreen.routeName,
|
||||
queryParameters: {MobileEmojiPickerScreen.pageTitle: title},
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.pageTitle: title,
|
||||
MobileEmojiPickerScreen.iconSelectedType: emoji.type.name,
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
if (result != null) {
|
||||
onSubmitted(result, null);
|
||||
onSubmitted(result.toSelectedResult(), null);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
|
||||
+2
-2
@@ -258,10 +258,10 @@ class _CalloutBlockComponentWidgetState
|
||||
placeholderText: placeholderText,
|
||||
textAlign: alignment?.toTextAlign ?? textAlign,
|
||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
textStyle,
|
||||
textStyleWithTextSpan(textSpan: textSpan),
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
placeholderTextStyle,
|
||||
placeholderTextStyleWithTextSpan(textSpan: textSpan),
|
||||
),
|
||||
textDirection: textDirection,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
|
||||
+3
-2
@@ -227,11 +227,12 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
value: pageStyleIconBloc,
|
||||
child: Expanded(
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
pageStyleIconBloc.add(
|
||||
PageStyleIconEvent.updateIcon(r, true),
|
||||
PageStyleIconEvent.updateIcon(r.data, true),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
if (!r.keepOpen) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+14
-10
@@ -468,9 +468,9 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
isPopoverOpen = true;
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onIconOrCoverChanged(icon: result);
|
||||
_popoverController.close();
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onIconOrCoverChanged(icon: r.data);
|
||||
if (!r.keepOpen) _popoverController.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -838,9 +838,7 @@ class _DocumentIconState extends State<DocumentIcon> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = EmojiIconWidget(
|
||||
emoji: widget.icon,
|
||||
);
|
||||
Widget child = EmojiIconWidget(emoji: widget.icon);
|
||||
|
||||
if (UniversalPlatform.isDesktopOrWeb) {
|
||||
child = AppFlowyPopover(
|
||||
@@ -852,9 +850,10 @@ class _DocumentIconState extends State<DocumentIcon> {
|
||||
child: child,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onChangeIcon(result);
|
||||
_popoverController.close();
|
||||
initialType: widget.icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onChangeIcon(r.data);
|
||||
if (!r.keepOpen) _popoverController.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -864,7 +863,12 @@ class _DocumentIconState extends State<DocumentIcon> {
|
||||
child: child,
|
||||
onTap: () async {
|
||||
final result = await context.push<EmojiIconData>(
|
||||
MobileEmojiPickerScreen.routeName,
|
||||
Uri(
|
||||
path: MobileEmojiPickerScreen.routeName,
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.iconSelectedType: widget.icon.type.name,
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
if (result != null) {
|
||||
widget.onChangeIcon(result);
|
||||
|
||||
+14
-5
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
@@ -67,10 +68,13 @@ class RawEmojiIconWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultEmoji = EmojiText(
|
||||
emoji: '❓',
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
final defaultEmoji = SizedBox(
|
||||
width: emojiSize,
|
||||
child: EmojiText(
|
||||
emoji: '❓',
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
try {
|
||||
switch (emoji.type) {
|
||||
@@ -82,9 +86,14 @@ class RawEmojiIconWidget extends StatelessWidget {
|
||||
);
|
||||
case FlowyIconType.icon:
|
||||
final iconData = IconsData.fromJson(jsonDecode(emoji.emoji));
|
||||
|
||||
/// Under the same width conditions, icons on macOS seem to appear
|
||||
/// larger than emojis, so 0.9 is used here to slightly reduce the
|
||||
/// size of the icons
|
||||
final iconSize = Platform.isMacOS ? emojiSize * 0.9 : emojiSize;
|
||||
return IconWidget(
|
||||
data: iconData,
|
||||
size: emojiSize,
|
||||
size: iconSize,
|
||||
);
|
||||
default:
|
||||
return defaultEmoji;
|
||||
|
||||
+5
-5
@@ -153,11 +153,6 @@ class MathEquationBlockComponentWidgetState
|
||||
),
|
||||
);
|
||||
|
||||
child = Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
@@ -174,6 +169,11 @@ class MathEquationBlockComponentWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
child = Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (UniversalPlatform.isDesktopOrWeb) {
|
||||
child = Stack(
|
||||
children: [
|
||||
|
||||
+2
-2
@@ -173,8 +173,8 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'@$formattedDate',
|
||||
|
||||
+5
-1
@@ -10,6 +10,7 @@ import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/shared/clipboard_state.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
@@ -510,7 +511,10 @@ class _MentionPageBlockContent extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (blockContent == null || blockContent.isEmpty) {
|
||||
return shouldDisplayViewName ? view.name : '';
|
||||
return shouldDisplayViewName
|
||||
? view.name
|
||||
.orDefault(LocaleKeys.menuAppHeader_defaultNewPageName.tr())
|
||||
: '';
|
||||
}
|
||||
|
||||
return shouldDisplayViewName
|
||||
|
||||
+11
-1
@@ -20,6 +20,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
abstract class AppFlowyMobileToolbarWidgetService {
|
||||
void closeItemMenu();
|
||||
|
||||
void closeKeyboard();
|
||||
|
||||
PropertyValueNotifier<bool> get showMenuNotifier;
|
||||
@@ -179,7 +180,13 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
// but in this case, we don't want to update the cached keyboard height.
|
||||
// This is because we want to keep the same height when the menu is shown.
|
||||
bool canUpdateCachedKeyboardHeight = true;
|
||||
ValueNotifier<double> cachedKeyboardHeight = ValueNotifier(0.0);
|
||||
|
||||
/// when the [_MobileToolbar] disposed before the keyboard height can be updated in time,
|
||||
/// there will be an issue with the height being 0
|
||||
/// this is used to globally record the height.
|
||||
static double _globalCachedKeyboardHeight = 0.0;
|
||||
ValueNotifier<double> cachedKeyboardHeight =
|
||||
ValueNotifier(_globalCachedKeyboardHeight);
|
||||
|
||||
// used to check if click the same item again
|
||||
int? selectedMenuIndex;
|
||||
@@ -408,6 +415,9 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (keyboardHeight > 0) {
|
||||
_globalCachedKeyboardHeight = keyboardHeight;
|
||||
}
|
||||
return SizedBox(
|
||||
height: keyboardHeight,
|
||||
child: (showingMenu && selectedMenuIndex != null)
|
||||
|
||||
+18
-15
@@ -19,26 +19,29 @@ class NumberedListIcon extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final textStyleConfiguration =
|
||||
context.read<EditorState>().editorStyle.textStyleConfiguration;
|
||||
final fontSize = textStyleConfiguration.text.fontSize ?? 16.0;
|
||||
final height =
|
||||
textStyleConfiguration.text.height ?? textStyleConfiguration.lineHeight;
|
||||
final combinedTextStyle = textStyle?.combine(textStyleConfiguration.text) ??
|
||||
textStyleConfiguration.text;
|
||||
final size = fontSize * height;
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: size,
|
||||
minHeight: size,
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 0.5, right: 8.0),
|
||||
alignment: Alignment.center,
|
||||
child: Center(
|
||||
child: Text(
|
||||
node.levelString,
|
||||
style: combinedTextStyle,
|
||||
strutStyle: StrutStyle.fromTextStyle(combinedTextStyle),
|
||||
textDirection: textDirection,
|
||||
final adjustedTextStyle = combinedTextStyle.copyWith(
|
||||
height: height,
|
||||
fontFeatures: [const FontFeature.tabularFigures()],
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(
|
||||
node.levelString,
|
||||
style: adjustedTextStyle,
|
||||
strutStyle: StrutStyle.fromTextStyle(combinedTextStyle),
|
||||
textHeightBehavior: TextHeightBehavior(
|
||||
applyHeightToFirstAscent:
|
||||
textStyleConfiguration.applyHeightToFirstAscent,
|
||||
applyHeightToLastDescent:
|
||||
textStyleConfiguration.applyHeightToLastDescent,
|
||||
leadingDistribution: textStyleConfiguration.leadingDistribution,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+8
-5
@@ -106,10 +106,13 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
child = MobileBlockActionButtons(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
child: child,
|
||||
child = Padding(
|
||||
padding: padding,
|
||||
child: MobileBlockActionButtons(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,7 +173,7 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40.0,
|
||||
),
|
||||
padding: padding,
|
||||
padding: UniversalPlatform.isMobile ? EdgeInsets.zero : padding,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
|
||||
+14
-10
@@ -35,7 +35,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
||||
builder: (context, state) {
|
||||
final icon = state.icon ?? EmojiIconData.none();
|
||||
return GestureDetector(
|
||||
onTap: () => _showIconSelector(context),
|
||||
onTap: () => _showIconSelector(context, icon),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 52,
|
||||
@@ -48,12 +48,15 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
||||
const HSpace(16.0),
|
||||
FlowyText(LocaleKeys.document_plugins_emoji.tr()),
|
||||
const Spacer(),
|
||||
RawEmojiIconWidget(
|
||||
emoji: icon.isNotEmpty
|
||||
? icon
|
||||
: EmojiIconData.emoji(LocaleKeys.pageStyle_none.tr()),
|
||||
emojiSize: icon.isNotEmpty ? 22.0 : 16.0,
|
||||
),
|
||||
icon.isEmpty
|
||||
? FlowyText(
|
||||
LocaleKeys.pageStyle_none.tr(),
|
||||
fontSize: 16.0,
|
||||
)
|
||||
: RawEmojiIconWidget(
|
||||
emoji: icon,
|
||||
emojiSize: 22.0,
|
||||
),
|
||||
const HSpace(6.0),
|
||||
const FlowySvg(FlowySvgs.m_page_style_arrow_right_s),
|
||||
const HSpace(12.0),
|
||||
@@ -66,7 +69,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showIconSelector(BuildContext context) {
|
||||
void _showIconSelector(BuildContext context, EmojiIconData icon) {
|
||||
Navigator.pop(context);
|
||||
final pageStyleIconBloc = PageStyleIconBloc(view: widget.view)
|
||||
..add(const PageStyleIconEvent.initial());
|
||||
@@ -85,11 +88,12 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
||||
value: pageStyleIconBloc,
|
||||
child: Expanded(
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
pageStyleIconBloc.add(
|
||||
PageStyleIconEvent.updateIcon(r, true),
|
||||
PageStyleIconEvent.updateIcon(r.data, true),
|
||||
);
|
||||
Navigator.pop(ctx);
|
||||
if (!r.keepOpen) Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Backspace key event.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
/// - mobile
|
||||
///
|
||||
final CommandShortcutEvent customBackspaceCommand = CommandShortcutEvent(
|
||||
key: 'backspace',
|
||||
getDescription: () => AppFlowyEditorL10n.current.cmdDeleteLeft,
|
||||
command: 'backspace, shift+backspace',
|
||||
handler: _backspaceCommandHandler,
|
||||
);
|
||||
|
||||
CommandShortcutEventHandler _backspaceCommandHandler = (editorState) {
|
||||
final selection = editorState.selection;
|
||||
final selectionType = editorState.selectionType;
|
||||
|
||||
if (selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final reason = editorState.selectionUpdateReason;
|
||||
|
||||
if (selectionType == SelectionType.block) {
|
||||
return _backspaceInBlockSelection(editorState);
|
||||
} else if (selection.isCollapsed) {
|
||||
return _backspaceInCollapsedSelection(editorState);
|
||||
} else if (reason == SelectionUpdateReason.selectAll) {
|
||||
return _backspaceInSelectAll(editorState);
|
||||
} else {
|
||||
return _backspaceInNotCollapsedSelection(editorState);
|
||||
}
|
||||
};
|
||||
|
||||
/// Handle backspace key event when selection is collapsed.
|
||||
CommandShortcutEventHandler _backspaceInCollapsedSelection = (editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final position = selection.start;
|
||||
final node = editorState.getNodeAtPath(position.path);
|
||||
if (node == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
// delete the entire node if the delta is empty
|
||||
if (node.delta == null) {
|
||||
transaction.deleteNode(node);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: position.path,
|
||||
),
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// Why do we use prevRunPosition instead of the position start offset?
|
||||
// Because some character's length > 1, for example, emoji.
|
||||
final index = node.delta!.prevRunePosition(position.offset);
|
||||
|
||||
if (index < 0) {
|
||||
// move this node to it's parent in below case.
|
||||
// the node's next is null
|
||||
// and the node's children is empty
|
||||
if (node.next == null &&
|
||||
node.children.isEmpty &&
|
||||
node.parent?.parent != null &&
|
||||
node.parent?.delta != null) {
|
||||
final path = node.parent!.path.next;
|
||||
transaction
|
||||
..deleteNode(node)
|
||||
..insertNode(path, node)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: path,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// If the deletion crosses columns and starts from the beginning position
|
||||
// skip the node deletion process
|
||||
// otherwise it will cause an error in table rendering.
|
||||
if (node.parent?.type == SimpleTableCellBlockKeys.type &&
|
||||
position.offset == 0) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
final Node? tableParent = node
|
||||
.findParent((element) => element.type == SimpleTableBlockKeys.type);
|
||||
Node? prevTableParent;
|
||||
final prev = node.previousNodeWhere((element) {
|
||||
prevTableParent = element
|
||||
.findParent((element) => element.type == SimpleTableBlockKeys.type);
|
||||
// break if only one is in a table or they're in different tables
|
||||
return tableParent != prevTableParent ||
|
||||
// merge with the previous node contains delta.
|
||||
element.delta != null;
|
||||
});
|
||||
// table nodes should be deleted using the table menu
|
||||
// in-table paragraphs should only be deleted inside the table
|
||||
if (prev != null && tableParent == prevTableParent) {
|
||||
assert(prev.delta != null);
|
||||
transaction
|
||||
..mergeText(prev, node)
|
||||
..insertNodes(
|
||||
// insert children to previous node
|
||||
prev.path.next,
|
||||
node.children.toList(),
|
||||
)
|
||||
..deleteNode(node)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: prev.path,
|
||||
offset: prev.delta!.length,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// do nothing if there is no previous node contains delta.
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Although the selection may be collapsed,
|
||||
// its length may not always be equal to 1 because some characters have a length greater than 1.
|
||||
transaction.deleteText(
|
||||
node,
|
||||
index,
|
||||
position.offset - index,
|
||||
);
|
||||
}
|
||||
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
/// Handle backspace key event when selection is not collapsed.
|
||||
CommandShortcutEventHandler _backspaceInNotCollapsedSelection = (editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
editorState.deleteSelectionV2(selection);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
CommandShortcutEventHandler _backspaceInBlockSelection = (editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || editorState.selectionType != SelectionType.block) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final transaction = editorState.transaction;
|
||||
transaction.deleteNodesAtPath(selection.start.path);
|
||||
editorState
|
||||
.apply(transaction)
|
||||
.then((value) => editorState.selectionType = null);
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
CommandShortcutEventHandler _backspaceInSelectAll = (editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
transaction.deleteNodes(nodes);
|
||||
editorState.apply(transaction);
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
extension on EditorState {
|
||||
Future<bool> deleteSelectionV2(Selection selection) async {
|
||||
// Nothing to do if the selection is collapsed.
|
||||
if (selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize the selection so that it is never reversed or extended.
|
||||
selection = selection.normalized;
|
||||
|
||||
// Start a new transaction.
|
||||
final transaction = this.transaction;
|
||||
|
||||
// Get the nodes that are fully or partially selected.
|
||||
final nodes = getNodesInSelection(selection);
|
||||
|
||||
// If only one node is selected, then we can just delete the selected text
|
||||
// or node.
|
||||
if (nodes.length == 1) {
|
||||
// If table cell is selected, clear the cell node child.
|
||||
final node = nodes.first.type == SimpleTableCellBlockKeys.type
|
||||
? nodes.first.children.first
|
||||
: nodes.first;
|
||||
if (node.delta != null) {
|
||||
transaction.deleteText(
|
||||
node,
|
||||
selection.startIndex,
|
||||
selection.length,
|
||||
);
|
||||
} else if (node.parent?.type != SimpleTableCellBlockKeys.type &&
|
||||
node.parent?.type != SimpleTableRowBlockKeys.type) {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, multiple nodes are selected, so we have to do more work.
|
||||
else {
|
||||
// The nodes are guaranteed to be in order, so we can determine which
|
||||
// nodes are at the beginning, middle, and end of the selection.
|
||||
assert(nodes.first.path < nodes.last.path);
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
final node = nodes[i];
|
||||
|
||||
// The first node is at the beginning of the selection.
|
||||
// All other nodes can be deleted.
|
||||
if (i != 0) {
|
||||
// Never delete a table cell node child
|
||||
if (node.parent?.type == SimpleTableCellBlockKeys.type) {
|
||||
if (!nodes.any((n) => n.id == node.parent?.parent?.id) &&
|
||||
node.delta != null) {
|
||||
transaction.deleteText(
|
||||
node,
|
||||
0,
|
||||
min(selection.end.offset, node.delta!.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
// If first node was inside table cell then it wasn't mergable to last
|
||||
// node, So we should not delete the last node. Just delete part of
|
||||
// the text inside selection
|
||||
else if (node.id == nodes.last.id &&
|
||||
nodes.first.parent?.type == SimpleTableCellBlockKeys.type) {
|
||||
transaction.deleteText(
|
||||
node,
|
||||
0,
|
||||
selection.end.offset,
|
||||
);
|
||||
} else if (node.type != SimpleTableCellBlockKeys.type &&
|
||||
node.type != SimpleTableRowBlockKeys.type) {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the last node is also a text node and not a node inside table cell,
|
||||
// and also the current node isn't inside table cell, then we can merge
|
||||
// the text between the two nodes.
|
||||
if (nodes.last.delta != null &&
|
||||
![node.parent?.type, nodes.last.parent?.type]
|
||||
.contains(SimpleTableCellBlockKeys.type)) {
|
||||
transaction.mergeText(
|
||||
node,
|
||||
nodes.last,
|
||||
leftOffset: selection.startIndex,
|
||||
rightOffset: selection.endIndex,
|
||||
);
|
||||
|
||||
// combine the children of the last node into the first node.
|
||||
final last = nodes.last;
|
||||
|
||||
if (last.children.isNotEmpty) {
|
||||
if (indentableBlockTypes.contains(node.type)) {
|
||||
transaction.insertNodes(
|
||||
node.path + [0],
|
||||
last.children,
|
||||
);
|
||||
} else {
|
||||
transaction.insertNodes(
|
||||
node.path.next,
|
||||
last.children,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, we can just delete the selected text.
|
||||
else {
|
||||
// If the last or first node is inside table we will only delete
|
||||
// selection part of first node.
|
||||
if (nodes.last.parent?.type == SimpleTableCellBlockKeys.type ||
|
||||
node.parent?.type == SimpleTableCellBlockKeys.type) {
|
||||
transaction.deleteText(
|
||||
node,
|
||||
selection.startIndex,
|
||||
node.delta!.length - selection.startIndex,
|
||||
);
|
||||
} else {
|
||||
transaction.deleteText(
|
||||
node,
|
||||
selection.startIndex,
|
||||
selection.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After the selection is deleted, we want to move the selection to the
|
||||
// beginning of the deleted selection.
|
||||
transaction.afterSelection = selection.collapse(atStart: true);
|
||||
|
||||
// Apply the transaction.
|
||||
await apply(transaction);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/backspace_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -37,6 +38,8 @@ List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
|
||||
...customTextAlignCommands,
|
||||
|
||||
customBackspaceCommand,
|
||||
|
||||
// remove standard shortcuts for copy, cut, paste, todo
|
||||
...standardCommandShortcutEvents
|
||||
..removeWhere(
|
||||
|
||||
+1
-1
@@ -456,7 +456,7 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
return context.simpleTableDefaultHeaderColor;
|
||||
}
|
||||
|
||||
return Theme.of(context).colorScheme.surface;
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
bool _isInHeader() {
|
||||
|
||||
+8
@@ -86,6 +86,9 @@ class SimpleTableContext {
|
||||
/// This value is available on mobile only
|
||||
final ValueNotifier<int?> isReorderingHitIndex = ValueNotifier(null);
|
||||
|
||||
/// Scroll controller for the table
|
||||
ScrollController? horizontalScrollController;
|
||||
|
||||
void _onHoveringOnColumnsAndRowsChanged() {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
@@ -269,6 +272,8 @@ class SimpleTableConstants {
|
||||
static const actionSheetContentSectionHeight = 44.0;
|
||||
static const actionSheetNormalActionSectionHeight = 48.0;
|
||||
static const actionSheetButtonRadius = 12.0;
|
||||
|
||||
static const actionSheetBottomSheetHeight = 320.0;
|
||||
}
|
||||
|
||||
enum SimpleTableBorderRenderType {
|
||||
@@ -308,4 +313,7 @@ extension SimpleTableColors on BuildContext {
|
||||
Color get simpleTableInsertActionBackgroundColor => Theme.of(this).isLightMode
|
||||
? const Color(0xFFF2F2F7)
|
||||
: const Color(0xFF2D3036);
|
||||
|
||||
Color? get simpleTableQuickActionBackgroundColor =>
|
||||
Theme.of(this).isLightMode ? null : const Color(0xFFBBC3CD);
|
||||
}
|
||||
+12
-2
@@ -438,6 +438,8 @@ class SimpleTableActionMenu extends StatelessWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
final beforeSelection = editorState.selection;
|
||||
|
||||
// increase the keep editor focus notifier to prevent the editor from losing focus
|
||||
keepEditorFocusNotifier.increase();
|
||||
|
||||
@@ -469,6 +471,8 @@ class SimpleTableActionMenu extends StatelessWidget {
|
||||
context,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
useSafeArea: false,
|
||||
enablePadding: false,
|
||||
builder: (context) => Provider.value(
|
||||
value: simpleTableContext,
|
||||
child: SimpleTableBottomSheet(
|
||||
@@ -482,7 +486,13 @@ class SimpleTableActionMenu extends StatelessWidget {
|
||||
keepEditorFocusNotifier.decrease();
|
||||
|
||||
// remove the extra info
|
||||
editorState.selectionType = null;
|
||||
editorState.selectionExtraInfo = null;
|
||||
if (beforeSelection != null) {
|
||||
await editorState.updateSelectionWithReason(
|
||||
beforeSelection,
|
||||
customSelectionType: SelectionType.inline,
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
extraInfo: {},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -137,8 +137,8 @@ extension TableContentOperation on EditorState {
|
||||
final cell = columnIndex >= row.children.length
|
||||
? row.children.last
|
||||
: row.children[columnIndex];
|
||||
final startNode = cell.getFirstChildIndex();
|
||||
final endNode = cell.getLastChildIndex();
|
||||
final startNode = cell.getFirstFocusableChild();
|
||||
final endNode = cell.getLastFocusableChild();
|
||||
if (startNode == null || endNode == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -200,8 +200,8 @@ extension TableContentOperation on EditorState {
|
||||
final row = tableNode.children[rowIndex];
|
||||
for (var i = 0; i < row.children.length; i++) {
|
||||
final cell = row.children[i];
|
||||
final startNode = cell.getFirstChildIndex();
|
||||
final endNode = cell.getLastChildIndex();
|
||||
final startNode = cell.getFirstFocusableChild();
|
||||
final endNode = cell.getLastFocusableChild();
|
||||
if (startNode == null || endNode == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -255,8 +255,8 @@ extension TableContentOperation on EditorState {
|
||||
final row = tableNode.children[i];
|
||||
for (var j = 0; j < row.children.length; j++) {
|
||||
final cell = row.children[j];
|
||||
final startNode = cell.getFirstChildIndex();
|
||||
final endNode = cell.getLastChildIndex();
|
||||
final startNode = cell.getFirstFocusableChild();
|
||||
final endNode = cell.getLastFocusableChild();
|
||||
if (startNode == null || endNode == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+241
@@ -104,6 +104,18 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final rowBoldAttributes = _remapSource(
|
||||
this.rowBoldAttributes,
|
||||
index,
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final rowTextColors = _remapSource(
|
||||
this.rowTextColors,
|
||||
index,
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
@@ -112,6 +124,14 @@ extension TableMapOperation on Node {
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowAligns,
|
||||
rowAligns,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowBoldAttributes,
|
||||
rowBoldAttributes,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowTextColors,
|
||||
rowTextColors,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row insertion attributes: $e');
|
||||
@@ -167,6 +187,18 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final columnBoldAttributes = _remapSource(
|
||||
this.columnBoldAttributes,
|
||||
index,
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final columnTextColors = _remapSource(
|
||||
this.columnTextColors,
|
||||
index,
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final bool distributeColumnWidthsEvenly =
|
||||
attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly] ??
|
||||
false;
|
||||
@@ -189,6 +221,14 @@ extension TableMapOperation on Node {
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnWidths,
|
||||
columnWidths,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnBoldAttributes,
|
||||
columnBoldAttributes,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnTextColors,
|
||||
columnTextColors,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row insertion attributes: $e');
|
||||
@@ -238,6 +278,18 @@ extension TableMapOperation on Node {
|
||||
index,
|
||||
);
|
||||
|
||||
final (rowBoldAttributes, duplicatedRowBoldAttribute) =
|
||||
_findDuplicatedEntryAndRemap(
|
||||
this.rowBoldAttributes,
|
||||
index,
|
||||
);
|
||||
|
||||
final (rowTextColors, duplicatedRowTextColor) =
|
||||
_findDuplicatedEntryAndRemap(
|
||||
this.rowTextColors,
|
||||
index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
@@ -248,6 +300,16 @@ extension TableMapOperation on Node {
|
||||
SimpleTableBlockKeys.rowAligns,
|
||||
rowAligns,
|
||||
duplicatedEntry: duplicatedRowAlign,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowBoldAttributes,
|
||||
rowBoldAttributes,
|
||||
duplicatedEntry: duplicatedRowBoldAttribute,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowTextColors,
|
||||
rowTextColors,
|
||||
duplicatedEntry: duplicatedRowTextColor,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row insertion attributes: $e');
|
||||
@@ -304,6 +366,18 @@ extension TableMapOperation on Node {
|
||||
index,
|
||||
);
|
||||
|
||||
final (columnBoldAttributes, duplicatedColumnBoldAttribute) =
|
||||
_findDuplicatedEntryAndRemap(
|
||||
this.columnBoldAttributes,
|
||||
index,
|
||||
);
|
||||
|
||||
final (columnTextColors, duplicatedColumnTextColor) =
|
||||
_findDuplicatedEntryAndRemap(
|
||||
this.columnTextColors,
|
||||
index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnColors,
|
||||
@@ -319,6 +393,16 @@ extension TableMapOperation on Node {
|
||||
SimpleTableBlockKeys.columnWidths,
|
||||
columnWidths,
|
||||
duplicatedEntry: duplicatedColumnWidth,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnBoldAttributes,
|
||||
columnBoldAttributes,
|
||||
duplicatedEntry: duplicatedColumnBoldAttribute,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnTextColors,
|
||||
columnTextColors,
|
||||
duplicatedEntry: duplicatedColumnTextColor,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map column duplication attributes: $e');
|
||||
@@ -364,6 +448,7 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final columnAligns = _remapSource(
|
||||
this.columnAligns,
|
||||
index,
|
||||
@@ -371,6 +456,7 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final columnWidths = _remapSource(
|
||||
this.columnWidths,
|
||||
index,
|
||||
@@ -379,6 +465,22 @@ extension TableMapOperation on Node {
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final columnBoldAttributes = _remapSource(
|
||||
this.columnBoldAttributes,
|
||||
index,
|
||||
increment: false,
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final columnTextColors = _remapSource(
|
||||
this.columnTextColors,
|
||||
index,
|
||||
increment: false,
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnColors,
|
||||
@@ -391,6 +493,14 @@ extension TableMapOperation on Node {
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnWidths,
|
||||
columnWidths,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnBoldAttributes,
|
||||
columnBoldAttributes,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnTextColors,
|
||||
columnTextColors,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map column deletion attributes: $e');
|
||||
@@ -443,6 +553,22 @@ extension TableMapOperation on Node {
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final rowBoldAttributes = _remapSource(
|
||||
this.rowBoldAttributes,
|
||||
index,
|
||||
increment: false,
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final rowTextColors = _remapSource(
|
||||
this.rowTextColors,
|
||||
index,
|
||||
increment: false,
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
@@ -451,6 +577,14 @@ extension TableMapOperation on Node {
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowAligns,
|
||||
rowAligns,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowBoldAttributes,
|
||||
rowBoldAttributes,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowTextColors,
|
||||
rowTextColors,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row deletion attributes: $e');
|
||||
@@ -531,6 +665,10 @@ extension TableMapOperation on Node {
|
||||
final duplicatedColumnColor = this.columnColors[fromIndex.toString()];
|
||||
final duplicatedColumnAlign = this.columnAligns[fromIndex.toString()];
|
||||
final duplicatedColumnWidth = this.columnWidths[fromIndex.toString()];
|
||||
final duplicatedColumnBoldAttribute =
|
||||
this.columnBoldAttributes[fromIndex.toString()];
|
||||
final duplicatedColumnTextColor =
|
||||
this.columnTextColors[fromIndex.toString()];
|
||||
|
||||
/// Case 1: fromIndex > toIndex
|
||||
/// Before:
|
||||
@@ -619,6 +757,34 @@ extension TableMapOperation on Node {
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final columnBoldAttributes = _remapSource(
|
||||
this.columnBoldAttributes,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final columnTextColors = _remapSource(
|
||||
this.columnTextColors,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnColors,
|
||||
@@ -652,6 +818,28 @@ extension TableMapOperation on Node {
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnBoldAttributes,
|
||||
columnBoldAttributes,
|
||||
duplicatedEntry: duplicatedColumnBoldAttribute != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedColumnBoldAttribute,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnTextColors,
|
||||
columnTextColors,
|
||||
duplicatedEntry: duplicatedColumnTextColor != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedColumnTextColor,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map column deletion attributes: $e');
|
||||
@@ -667,6 +855,9 @@ extension TableMapOperation on Node {
|
||||
try {
|
||||
final duplicatedRowColor = this.rowColors[fromIndex.toString()];
|
||||
final duplicatedRowAlign = this.rowAligns[fromIndex.toString()];
|
||||
final duplicatedRowBoldAttribute =
|
||||
this.rowBoldAttributes[fromIndex.toString()];
|
||||
final duplicatedRowTextColor = this.rowTextColors[fromIndex.toString()];
|
||||
|
||||
/// Example:
|
||||
/// Case 1: fromIndex > toIndex
|
||||
@@ -742,6 +933,34 @@ extension TableMapOperation on Node {
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final rowBoldAttributes = _remapSource(
|
||||
this.rowBoldAttributes,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final rowTextColors = _remapSource(
|
||||
this.rowTextColors,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
@@ -764,6 +983,28 @@ extension TableMapOperation on Node {
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowBoldAttributes,
|
||||
rowBoldAttributes,
|
||||
duplicatedEntry: duplicatedRowBoldAttribute != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedRowBoldAttribute,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowTextColors,
|
||||
rowTextColors,
|
||||
duplicatedEntry: duplicatedRowTextColor != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedRowTextColor,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row reordering attributes: $e');
|
||||
|
||||
+4
-4
@@ -805,21 +805,21 @@ extension TableNodeExtension on Node {
|
||||
/// Get first focusable child in the table cell.
|
||||
///
|
||||
/// If the current node is not a table cell node, it will return null.
|
||||
Node? getFirstChildIndex() {
|
||||
Node? getFirstFocusableChild() {
|
||||
if (children.isEmpty) {
|
||||
return this;
|
||||
}
|
||||
return children.first.getFirstChildIndex();
|
||||
return children.first.getFirstFocusableChild();
|
||||
}
|
||||
|
||||
/// Get last focusable child in the table cell.
|
||||
///
|
||||
/// If the current node is not a table cell node, it will return null.
|
||||
Node? getLastChildIndex() {
|
||||
Node? getLastFocusableChild() {
|
||||
if (children.isEmpty) {
|
||||
return this;
|
||||
}
|
||||
return children.last.getLastChildIndex();
|
||||
return children.last.getLastFocusableChild();
|
||||
}
|
||||
|
||||
/// Get table align of column
|
||||
|
||||
+67
@@ -117,6 +117,8 @@ extension TableOptionOperation on EditorState {
|
||||
required Node tableCellNode,
|
||||
required TableAlign align,
|
||||
}) async {
|
||||
await clearColumnTextAlign(tableCellNode: tableCellNode);
|
||||
|
||||
final columnIndex = tableCellNode.columnIndex;
|
||||
await _updateTableAttributes(
|
||||
tableCellNode: tableCellNode,
|
||||
@@ -144,6 +146,8 @@ extension TableOptionOperation on EditorState {
|
||||
required Node tableCellNode,
|
||||
required TableAlign align,
|
||||
}) async {
|
||||
await clearRowTextAlign(tableCellNode: tableCellNode);
|
||||
|
||||
final rowIndex = tableCellNode.rowIndex;
|
||||
await _updateTableAttributes(
|
||||
tableCellNode: tableCellNode,
|
||||
@@ -385,4 +389,67 @@ extension TableOptionOperation on EditorState {
|
||||
transaction.updateNode(parentTableNode, attributes);
|
||||
await apply(transaction);
|
||||
}
|
||||
|
||||
/// Clear the text align of the column at the index where the table cell node is located.
|
||||
Future<void> clearColumnTextAlign({
|
||||
required Node tableCellNode,
|
||||
}) async {
|
||||
final parentTableNode = tableCellNode.parentTableNode;
|
||||
if (parentTableNode == null) {
|
||||
Log.warn('parent table node is null');
|
||||
return;
|
||||
}
|
||||
final columnIndex = tableCellNode.columnIndex;
|
||||
final transaction = this.transaction;
|
||||
for (var i = 0; i < parentTableNode.rowLength; i++) {
|
||||
final cell = parentTableNode.getTableCellNode(
|
||||
rowIndex: i,
|
||||
columnIndex: columnIndex,
|
||||
);
|
||||
if (cell == null) {
|
||||
continue;
|
||||
}
|
||||
for (final child in cell.children) {
|
||||
transaction.updateNode(child, {
|
||||
blockComponentAlign: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
await apply(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the text align of the row at the index where the table cell node is located.
|
||||
Future<void> clearRowTextAlign({
|
||||
required Node tableCellNode,
|
||||
}) async {
|
||||
final parentTableNode = tableCellNode.parentTableNode;
|
||||
if (parentTableNode == null) {
|
||||
Log.warn('parent table node is null');
|
||||
return;
|
||||
}
|
||||
final rowIndex = tableCellNode.rowIndex;
|
||||
final transaction = this.transaction;
|
||||
for (var i = 0; i < parentTableNode.columnLength; i++) {
|
||||
final cell = parentTableNode.getTableCellNode(
|
||||
rowIndex: rowIndex,
|
||||
columnIndex: i,
|
||||
);
|
||||
if (cell == null) {
|
||||
continue;
|
||||
}
|
||||
for (final child in cell.children) {
|
||||
transaction.updateNode(
|
||||
child,
|
||||
{
|
||||
blockComponentAlign: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
await apply(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -40,6 +42,7 @@ KeyEventResult _arrowDownInTableCellHandler(EditorState editorState) {
|
||||
if (rowIndex == parentTableNode.rowLength - 1) {
|
||||
// focus on the next block
|
||||
final nextNode = tableCellNode.next;
|
||||
final nextBlock = tableCellNode.parentTableNode?.next;
|
||||
if (nextNode != null) {
|
||||
final nextFocusableSibling = parentTableNode.getNextFocusableSibling();
|
||||
if (nextFocusableSibling != null) {
|
||||
@@ -51,6 +54,16 @@ KeyEventResult _arrowDownInTableCellHandler(EditorState editorState) {
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (nextBlock != null) {
|
||||
if (nextBlock.type != SimpleTableBlockKeys.type) {
|
||||
newSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: nextBlock.path,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return tableNavigationArrowDownCommand.handler(editorState);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// focus on next cell in the same column
|
||||
|
||||
+2
@@ -4,10 +4,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart';
|
||||
|
||||
final simpleTableCommands = [
|
||||
tableNavigationArrowDownCommand,
|
||||
arrowUpInTableCell,
|
||||
arrowDownInTableCell,
|
||||
arrowLeftInTableCell,
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final CommandShortcutEvent tableNavigationArrowDownCommand =
|
||||
CommandShortcutEvent(
|
||||
key: 'table navigation',
|
||||
getDescription: () => 'table navigation',
|
||||
command: 'arrow down',
|
||||
handler: _tableNavigationArrowDownHandler,
|
||||
);
|
||||
|
||||
KeyEventResult _tableNavigationArrowDownHandler(EditorState editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final nextNode = editorState.getNodeAtPath(selection.start.path.next);
|
||||
if (nextNode == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
if (nextNode.type == SimpleTableBlockKeys.type) {
|
||||
final firstCell = nextNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
|
||||
if (firstCell != null) {
|
||||
final firstFocusableChild = firstCell.getFirstFocusableChild();
|
||||
if (firstFocusableChild != null) {
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(
|
||||
Position(path: firstFocusableChild.path),
|
||||
),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
+8
@@ -47,8 +47,16 @@ class _DesktopSimpleTableWidgetState extends State<DesktopSimpleTableWidget> {
|
||||
final scrollController = ScrollController();
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
simpleTableContext.horizontalScrollController = scrollController;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
simpleTableContext.horizontalScrollController = null;
|
||||
scrollController.dispose();
|
||||
|
||||
super.dispose();
|
||||
|
||||
+8
@@ -47,8 +47,16 @@ class _MobileSimpleTableWidgetState extends State<MobileSimpleTableWidget> {
|
||||
final scrollController = ScrollController();
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
simpleTableContext.horizontalScrollController = scrollController;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
simpleTableContext.horizontalScrollController = null;
|
||||
scrollController.dispose();
|
||||
|
||||
super.dispose();
|
||||
|
||||
+74
-51
@@ -61,19 +61,12 @@ class SimpleTableCellQuickActions extends ISimpleTableBottomSheetActions {
|
||||
SimpleTableMoreAction.copy,
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: getIt<ClipboardService>().getData(),
|
||||
builder: (context, snapshot) {
|
||||
final hasContent = snapshot.data?.tableJson != null;
|
||||
return SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.paste,
|
||||
isEnabled: hasContent,
|
||||
onTap: () => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.paste,
|
||||
),
|
||||
);
|
||||
},
|
||||
SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.paste,
|
||||
onTap: () => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.paste,
|
||||
),
|
||||
),
|
||||
SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.delete,
|
||||
@@ -208,8 +201,10 @@ class SimpleTableQuickAction extends StatelessWidget {
|
||||
onTapUp: isEnabled ? onTap : null,
|
||||
child: FlowySvg(
|
||||
type.leftIconSvg,
|
||||
blendMode: null,
|
||||
blendMode:
|
||||
type == SimpleTableMoreAction.delete ? null : BlendMode.srcIn,
|
||||
size: const Size.square(24),
|
||||
color: context.simpleTableQuickActionBackgroundColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -244,18 +239,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
|
||||
SimpleTableInsertAction(
|
||||
type: SimpleTableMoreAction.insertAbove,
|
||||
enableLeftBorder: true,
|
||||
onTap: () => _onActionTap(
|
||||
onTap: (increaseCounter) async => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.insertAbove,
|
||||
type: SimpleTableMoreAction.insertAbove,
|
||||
increaseCounter: increaseCounter,
|
||||
),
|
||||
),
|
||||
const HSpace(2),
|
||||
SimpleTableInsertAction(
|
||||
type: SimpleTableMoreAction.insertBelow,
|
||||
enableRightBorder: true,
|
||||
onTap: () => _onActionTap(
|
||||
onTap: (increaseCounter) async => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.insertBelow,
|
||||
type: SimpleTableMoreAction.insertBelow,
|
||||
increaseCounter: increaseCounter,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -265,18 +262,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
|
||||
SimpleTableInsertAction(
|
||||
type: SimpleTableMoreAction.insertLeft,
|
||||
enableLeftBorder: true,
|
||||
onTap: () => _onActionTap(
|
||||
onTap: (increaseCounter) async => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.insertLeft,
|
||||
type: SimpleTableMoreAction.insertLeft,
|
||||
increaseCounter: increaseCounter,
|
||||
),
|
||||
),
|
||||
const HSpace(2),
|
||||
SimpleTableInsertAction(
|
||||
type: SimpleTableMoreAction.insertRight,
|
||||
enableRightBorder: true,
|
||||
onTap: () => _onActionTap(
|
||||
onTap: (increaseCounter) async => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.insertRight,
|
||||
type: SimpleTableMoreAction.insertRight,
|
||||
increaseCounter: increaseCounter,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -284,7 +283,11 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
|
||||
};
|
||||
}
|
||||
|
||||
void _onActionTap(BuildContext context, SimpleTableMoreAction type) {
|
||||
Future<void> _onActionTap(
|
||||
BuildContext context, {
|
||||
required SimpleTableMoreAction type,
|
||||
required int increaseCounter,
|
||||
}) async {
|
||||
final simpleTableContext = context.read<SimpleTableContext>();
|
||||
final tableNode = cellNode.parentTableNode;
|
||||
if (tableNode == null) {
|
||||
@@ -296,34 +299,48 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
|
||||
case SimpleTableMoreAction.insertAbove:
|
||||
// update the highlight status for the selecting row
|
||||
simpleTableContext.selectingRow.value = cellNode.rowIndex + 1;
|
||||
editorState.insertRowInTable(
|
||||
await editorState.insertRowInTable(
|
||||
tableNode,
|
||||
cellNode.rowIndex,
|
||||
);
|
||||
case SimpleTableMoreAction.insertBelow:
|
||||
editorState.insertRowInTable(
|
||||
await editorState.insertRowInTable(
|
||||
tableNode,
|
||||
cellNode.rowIndex + 1,
|
||||
);
|
||||
// scroll to the next cell position
|
||||
editorState.scrollService?.scrollTo(
|
||||
SimpleTableConstants.defaultRowHeight,
|
||||
duration: Durations.short3,
|
||||
);
|
||||
case SimpleTableMoreAction.insertLeft:
|
||||
// update the highlight status for the selecting column
|
||||
simpleTableContext.selectingColumn.value = cellNode.columnIndex + 1;
|
||||
editorState.insertColumnInTable(
|
||||
await editorState.insertColumnInTable(
|
||||
tableNode,
|
||||
cellNode.columnIndex,
|
||||
);
|
||||
case SimpleTableMoreAction.insertRight:
|
||||
editorState.insertColumnInTable(
|
||||
await editorState.insertColumnInTable(
|
||||
tableNode,
|
||||
cellNode.columnIndex + 1,
|
||||
);
|
||||
final horizontalScrollController =
|
||||
simpleTableContext.horizontalScrollController;
|
||||
if (horizontalScrollController != null) {
|
||||
final previousWidth = horizontalScrollController.offset;
|
||||
horizontalScrollController.jumpTo(
|
||||
previousWidth + SimpleTableConstants.defaultColumnWidth,
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
assert(false, 'Unsupported action: $type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTableInsertAction extends StatelessWidget {
|
||||
class SimpleTableInsertAction extends StatefulWidget {
|
||||
const SimpleTableInsertAction({
|
||||
super.key,
|
||||
required this.type,
|
||||
@@ -335,7 +352,16 @@ class SimpleTableInsertAction extends StatelessWidget {
|
||||
final SimpleTableMoreAction type;
|
||||
final bool enableLeftBorder;
|
||||
final bool enableRightBorder;
|
||||
final void Function() onTap;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
@override
|
||||
State<SimpleTableInsertAction> createState() =>
|
||||
_SimpleTableInsertActionState();
|
||||
}
|
||||
|
||||
class _SimpleTableInsertActionState extends State<SimpleTableInsertAction> {
|
||||
// used to count how many times the action is tapped
|
||||
int increaseCounter = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -346,19 +372,19 @@ class SimpleTableInsertAction extends StatelessWidget {
|
||||
shape: _buildBorder(),
|
||||
),
|
||||
child: AnimatedGestureDetector(
|
||||
onTapUp: onTap,
|
||||
onTapUp: () => widget.onTap(increaseCounter++),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: FlowySvg(
|
||||
type.leftIconSvg,
|
||||
widget.type.leftIconSvg,
|
||||
size: const Size.square(22),
|
||||
),
|
||||
),
|
||||
FlowyText(
|
||||
type.name,
|
||||
widget.type.name,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
),
|
||||
@@ -375,10 +401,10 @@ class SimpleTableInsertAction extends StatelessWidget {
|
||||
);
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: enableLeftBorder ? radius : Radius.zero,
|
||||
bottomLeft: enableLeftBorder ? radius : Radius.zero,
|
||||
topRight: enableRightBorder ? radius : Radius.zero,
|
||||
bottomRight: enableRightBorder ? radius : Radius.zero,
|
||||
topLeft: widget.enableLeftBorder ? radius : Radius.zero,
|
||||
bottomLeft: widget.enableLeftBorder ? radius : Radius.zero,
|
||||
topRight: widget.enableRightBorder ? radius : Radius.zero,
|
||||
bottomRight: widget.enableRightBorder ? radius : Radius.zero,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -538,7 +564,11 @@ class SimpleTableCellActionButtons extends ISimpleTableBottomSheetActions {
|
||||
}
|
||||
|
||||
// close the action menu
|
||||
Navigator.of(context).pop();
|
||||
// keep the action menu open if the action is enable header row or enable header column
|
||||
if (action != SimpleTableMoreAction.enableHeaderRow &&
|
||||
action != SimpleTableMoreAction.enableHeaderColumn) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,7 +623,7 @@ class _SimpleTableHeaderActionButtonState
|
||||
child: CupertinoSwitch(
|
||||
value: value,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (_) {},
|
||||
onChanged: (_) => _toggle(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1199,19 +1229,12 @@ class SimpleTableQuickActions extends StatelessWidget {
|
||||
SimpleTableMoreAction.copy,
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: getIt<ClipboardService>().getData(),
|
||||
builder: (context, snapshot) {
|
||||
final hasContent = snapshot.data?.tableJson != null;
|
||||
return SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.paste,
|
||||
isEnabled: hasContent,
|
||||
onTap: () => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.paste,
|
||||
),
|
||||
);
|
||||
},
|
||||
SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.paste,
|
||||
onTap: () => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.paste,
|
||||
),
|
||||
),
|
||||
SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.delete,
|
||||
|
||||
+2
@@ -199,8 +199,10 @@ class _SimpleTableMobileReorderButtonState
|
||||
// show the bottom sheet
|
||||
await showMobileBottomSheet(
|
||||
context,
|
||||
useSafeArea: false,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
enablePadding: false,
|
||||
builder: (context) => Provider.value(
|
||||
value: simpleTableContext,
|
||||
child: SimpleTableCellBottomSheet(
|
||||
|
||||
+6
-1
@@ -42,7 +42,12 @@ class SimpleTableAddColumnAndRowHoverButton extends StatelessWidget {
|
||||
SimpleTableConstants.addColumnAndRowButtonBottomPadding,
|
||||
right: SimpleTableConstants.addColumnButtonPadding,
|
||||
child: SimpleTableAddColumnAndRowButton(
|
||||
onTap: () => editorState.addColumnAndRowInTable(node),
|
||||
onTap: () {
|
||||
// cancel the selection to avoid flashing the selection
|
||||
editorState.selection = null;
|
||||
|
||||
editorState.addColumnAndRowInTable(node);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
|
||||
+6
-2
@@ -81,8 +81,12 @@ class _SimpleTableAddColumnHoverButtonState
|
||||
child: Opacity(
|
||||
opacity: shouldShow ? 1.0 : 0.0,
|
||||
child: SimpleTableAddColumnButton(
|
||||
onTap: () =>
|
||||
widget.editorState.addColumnInTable(widget.tableNode),
|
||||
onTap: () {
|
||||
// cancel the selection to avoid flashing the selection
|
||||
widget.editorState.selection = null;
|
||||
|
||||
widget.editorState.addColumnInTable(widget.tableNode);
|
||||
},
|
||||
onHorizontalDragStart: (details) {
|
||||
context.read<SimpleTableContext>().isDraggingColumn = true;
|
||||
startDraggingOffset = details.globalPosition;
|
||||
|
||||
+8
-3
@@ -87,9 +87,14 @@ class _SimpleTableAddRowHoverButtonState
|
||||
SimpleTableConstants.cellBorderWidth,
|
||||
right: SimpleTableConstants.addRowButtonRightPadding,
|
||||
child: SimpleTableAddRowButton(
|
||||
onTap: () => widget.editorState.addRowInTable(
|
||||
widget.tableNode,
|
||||
),
|
||||
onTap: () {
|
||||
// cancel the selection to avoid flashing the selection
|
||||
widget.editorState.selection = null;
|
||||
|
||||
widget.editorState.addRowInTable(
|
||||
widget.tableNode,
|
||||
);
|
||||
},
|
||||
onVerticalDragStart: (details) {
|
||||
context.read<SimpleTableContext>().isDraggingRow = true;
|
||||
startDraggingOffset = details.globalPosition;
|
||||
|
||||
+59
-3
@@ -28,11 +28,13 @@ class SimpleTableCellBottomSheet extends StatefulWidget {
|
||||
required this.type,
|
||||
required this.cellNode,
|
||||
required this.editorState,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
final SimpleTableMoreActionType type;
|
||||
final Node cellNode;
|
||||
final EditorState editorState;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
@override
|
||||
State<SimpleTableCellBottomSheet> createState() =>
|
||||
@@ -81,7 +83,9 @@ class _SimpleTableCellBottomSheetState
|
||||
_buildHeader(),
|
||||
|
||||
// content
|
||||
..._buildContent(),
|
||||
...menuState == _SimpleTableBottomSheetMenuState.cellActionMenu
|
||||
? _buildScrollableContent()
|
||||
: _buildNonScrollableContent(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -102,18 +106,51 @@ class _SimpleTableCellBottomSheetState
|
||||
return BottomSheetHeader(
|
||||
showBackButton: false,
|
||||
showCloseButton: true,
|
||||
showDoneButton: false,
|
||||
showDoneButton: true,
|
||||
showRemoveButton: false,
|
||||
title: widget.type.name.capitalize(),
|
||||
onClose: () => setState(() {
|
||||
menuState = _SimpleTableBottomSheetMenuState.cellActionMenu;
|
||||
}),
|
||||
onDone: (_) => Navigator.pop(context),
|
||||
);
|
||||
default:
|
||||
throw UnimplementedError('Unsupported menu state: $menuState');
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildScrollableContent() {
|
||||
return [
|
||||
SizedBox(
|
||||
height: SimpleTableConstants.actionSheetBottomSheetHeight,
|
||||
child: Scrollbar(
|
||||
controller: widget.scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: widget.scrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._buildContent(),
|
||||
|
||||
// safe area padding
|
||||
VSpace(context.bottomSheetPadding() * 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildNonScrollableContent() {
|
||||
return [
|
||||
..._buildContent(),
|
||||
|
||||
// safe area padding
|
||||
VSpace(context.bottomSheetPadding()),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildContent() {
|
||||
switch (menuState) {
|
||||
case _SimpleTableBottomSheetMenuState.cellActionMenu:
|
||||
@@ -296,10 +333,12 @@ class SimpleTableBottomSheet extends StatefulWidget {
|
||||
super.key,
|
||||
required this.tableNode,
|
||||
required this.editorState,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
final Node tableNode;
|
||||
final EditorState editorState;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
@override
|
||||
State<SimpleTableBottomSheet> createState() => _SimpleTableBottomSheetState();
|
||||
@@ -321,7 +360,24 @@ class _SimpleTableBottomSheetState extends State<SimpleTableBottomSheet> {
|
||||
_buildHeader(),
|
||||
|
||||
// content
|
||||
..._buildContent(),
|
||||
SizedBox(
|
||||
height: SimpleTableConstants.actionSheetBottomSheetHeight,
|
||||
child: Scrollbar(
|
||||
controller: widget.scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: widget.scrollController,
|
||||
child: Column(
|
||||
children: [
|
||||
// content
|
||||
..._buildContent(),
|
||||
|
||||
// safe area padding
|
||||
VSpace(context.bottomSheetPadding() * 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+64
-7
@@ -31,12 +31,16 @@ class _SimpleTableMoreActionPopupState
|
||||
|
||||
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
|
||||
|
||||
late final simpleTableContext = context.read<SimpleTableContext>();
|
||||
Node? tableNode;
|
||||
Node? tableCellNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final tableCellNode =
|
||||
context.read<SimpleTableContext>().hoveringTableCell.value;
|
||||
tableCellNode = context.read<SimpleTableContext>().hoveringTableCell.value;
|
||||
tableNode = tableCellNode?.parentTableNode;
|
||||
gestureInterceptor = SelectionGestureInterceptor(
|
||||
key: 'simple_table_more_action_popup_interceptor_${tableCellNode?.id}',
|
||||
canTap: (details) => !_isTapInBounds(details.globalPosition),
|
||||
@@ -59,10 +63,6 @@ class _SimpleTableMoreActionPopupState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final simpleTableContext = context.read<SimpleTableContext>();
|
||||
final tableCellNode = simpleTableContext.hoveringTableCell.value;
|
||||
final tableNode = tableCellNode?.parentTableNode;
|
||||
|
||||
if (tableNode == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@@ -70,6 +70,9 @@ class _SimpleTableMoreActionPopupState
|
||||
return AppFlowyPopover(
|
||||
onOpen: () => _onOpen(tableCellNode: tableCellNode),
|
||||
onClose: () => _onClose(),
|
||||
canClose: () async {
|
||||
return true;
|
||||
},
|
||||
direction: widget.type == SimpleTableMoreActionType.row
|
||||
? PopoverDirection.bottomWithCenterAligned
|
||||
: PopoverDirection.bottomWithLeftAligned,
|
||||
@@ -81,7 +84,7 @@ class _SimpleTableMoreActionPopupState
|
||||
child: SimpleTableDraggableReorderButton(
|
||||
editorState: editorState,
|
||||
simpleTableContext: simpleTableContext,
|
||||
node: tableNode,
|
||||
node: tableNode!,
|
||||
index: widget.index,
|
||||
isShowingMenu: widget.isShowingMenu,
|
||||
type: widget.type,
|
||||
@@ -455,6 +458,21 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
final columnIndex = node.columnIndex;
|
||||
final editorState = context.read<EditorState>();
|
||||
editorState.insertColumnInTable(table, columnIndex);
|
||||
|
||||
final cell = table.getTableCellNode(
|
||||
rowIndex: 0,
|
||||
columnIndex: columnIndex,
|
||||
);
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update selection
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(
|
||||
path: cell.path.child(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _insertColumnRight() {
|
||||
@@ -466,6 +484,21 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
final columnIndex = node.columnIndex;
|
||||
final editorState = context.read<EditorState>();
|
||||
editorState.insertColumnInTable(table, columnIndex + 1);
|
||||
|
||||
final cell = table.getTableCellNode(
|
||||
rowIndex: 0,
|
||||
columnIndex: columnIndex + 1,
|
||||
);
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update selection
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(
|
||||
path: cell.path.child(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _insertRowAbove() {
|
||||
@@ -477,6 +510,18 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
final rowIndex = node.rowIndex;
|
||||
final editorState = context.read<EditorState>();
|
||||
editorState.insertRowInTable(table, rowIndex);
|
||||
|
||||
final cell = table.getTableCellNode(rowIndex: rowIndex, columnIndex: 0);
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update selection
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(
|
||||
path: cell.path.child(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _insertRowBelow() {
|
||||
@@ -488,6 +533,18 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
final rowIndex = node.rowIndex;
|
||||
final editorState = context.read<EditorState>();
|
||||
editorState.insertRowInTable(table, rowIndex + 1);
|
||||
|
||||
final cell = table.getTableCellNode(rowIndex: rowIndex + 1, columnIndex: 0);
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update selection
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(
|
||||
path: cell.path.child(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deleteRow() {
|
||||
|
||||
+7
@@ -108,11 +108,18 @@ Future<bool> _showSlashMenu(
|
||||
singleColumn: singleColumn,
|
||||
style: style,
|
||||
);
|
||||
|
||||
// disable the keyboard service
|
||||
editorState.service.keyboardService?.disable();
|
||||
|
||||
if (!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||
await _selectionMenuService?.show();
|
||||
} else {
|
||||
await _selectionMenuService?.show();
|
||||
}
|
||||
|
||||
// enable the keyboard service
|
||||
editorState.service.keyboardService?.enable();
|
||||
}
|
||||
|
||||
if (shouldInsertSlash) {
|
||||
|
||||
+14
-7
@@ -1,8 +1,10 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';
|
||||
import 'package:appflowy/plugins/trash/application/trash_listener.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
@@ -15,7 +17,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -200,6 +201,8 @@ class SubPageBlockComponentState extends State<SubPageBlockComponent>
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final textStyle = textStyleWithTextSpan();
|
||||
|
||||
Widget child = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: MouseRegion(
|
||||
@@ -242,12 +245,9 @@ class SubPageBlockComponentState extends State<SubPageBlockComponent>
|
||||
children: [
|
||||
const HSpace(10),
|
||||
view.icon.value.isNotEmpty
|
||||
? FlowyText.emoji(
|
||||
view.icon.value,
|
||||
fontSize: textStyle.fontSize,
|
||||
lineHeight: textStyle.height,
|
||||
color:
|
||||
AFThemeExtension.of(context).strongText,
|
||||
? RawEmojiIconWidget(
|
||||
emoji: view.icon.toEmojiIconData(),
|
||||
emojiSize: textStyle.fontSize ?? 16.0,
|
||||
)
|
||||
: view.defaultIcon(),
|
||||
const HSpace(6),
|
||||
@@ -299,6 +299,13 @@ class SubPageBlockComponentState extends State<SubPageBlockComponent>
|
||||
);
|
||||
}
|
||||
|
||||
if (UniversalPlatform.isMobile) {
|
||||
child = Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
},
|
||||
);
|
||||
|
||||
+9
-3
@@ -312,7 +312,9 @@ class _ToggleListBlockComponentWidgetState
|
||||
placeholderText: placeholderText,
|
||||
lineHeight: 1.5,
|
||||
textSpanDecorator: (textSpan) {
|
||||
var result = textSpan.updateTextStyle(textStyle);
|
||||
var result = textSpan.updateTextStyle(
|
||||
textStyleWithTextSpan(textSpan: textSpan),
|
||||
);
|
||||
if (level != null) {
|
||||
result = result.updateTextStyle(
|
||||
widget.textStyleBuilder?.call(level),
|
||||
@@ -321,13 +323,17 @@ class _ToggleListBlockComponentWidgetState
|
||||
return result;
|
||||
},
|
||||
placeholderTextSpanDecorator: (textSpan) {
|
||||
var result = textSpan.updateTextStyle(textStyle);
|
||||
var result = textSpan.updateTextStyle(
|
||||
textStyleWithTextSpan(textSpan: textSpan),
|
||||
);
|
||||
if (level != null && widget.textStyleBuilder != null) {
|
||||
result = result.updateTextStyle(
|
||||
widget.textStyleBuilder?.call(level),
|
||||
);
|
||||
}
|
||||
return result.updateTextStyle(placeholderTextStyle);
|
||||
return result.updateTextStyle(
|
||||
placeholderTextStyleWithTextSpan(textSpan: textSpan),
|
||||
);
|
||||
},
|
||||
textDirection: textDirection,
|
||||
textAlign: alignment?.toTextAlign ?? textAlign,
|
||||
|
||||
@@ -166,9 +166,10 @@ class EditorStyleCustomizer {
|
||||
applyHeightToLastDescent: true,
|
||||
),
|
||||
textSpanDecorator: customizeAttributeDecorator,
|
||||
mobileDragHandleBallSize: const Size.square(12.0),
|
||||
magnifierSize: const Size(144, 96),
|
||||
textScaleFactor: textScaleFactor,
|
||||
mobileDragHandleLeftExtend: 12.0,
|
||||
mobileDragHandleWidthExtend: 24.0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,22 +138,36 @@ class DateReferenceService extends InlineActionsDelegate {
|
||||
final tomorrow = today.add(const Duration(days: 1));
|
||||
final yesterday = today.subtract(const Duration(days: 1));
|
||||
|
||||
_allOptions = [
|
||||
_itemFromDate(
|
||||
late InlineActionsMenuItem todayItem;
|
||||
late InlineActionsMenuItem tomorrowItem;
|
||||
late InlineActionsMenuItem yesterdayItem;
|
||||
|
||||
try {
|
||||
todayItem = _itemFromDate(
|
||||
today,
|
||||
LocaleKeys.relativeDates_today.tr(),
|
||||
[DateFormat.yMd(_locale).format(today)],
|
||||
),
|
||||
_itemFromDate(
|
||||
);
|
||||
tomorrowItem = _itemFromDate(
|
||||
tomorrow,
|
||||
LocaleKeys.relativeDates_tomorrow.tr(),
|
||||
[DateFormat.yMd(_locale).format(tomorrow)],
|
||||
),
|
||||
_itemFromDate(
|
||||
);
|
||||
yesterdayItem = _itemFromDate(
|
||||
yesterday,
|
||||
LocaleKeys.relativeDates_yesterday.tr(),
|
||||
[DateFormat.yMd(_locale).format(yesterday)],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
todayItem = _itemFromDate(today);
|
||||
tomorrowItem = _itemFromDate(tomorrow);
|
||||
yesterdayItem = _itemFromDate(yesterday);
|
||||
}
|
||||
|
||||
_allOptions = [
|
||||
todayItem,
|
||||
tomorrowItem,
|
||||
yesterdayItem,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -173,7 +187,17 @@ class DateReferenceService extends InlineActionsDelegate {
|
||||
String? label,
|
||||
List<String>? keywords,
|
||||
]) {
|
||||
final labelStr = label ?? DateFormat.yMd(_locale).format(date);
|
||||
late String labelStr;
|
||||
if (label != null) {
|
||||
labelStr = label;
|
||||
} else {
|
||||
try {
|
||||
labelStr = DateFormat.yMd(_locale).format(date);
|
||||
} catch (e) {
|
||||
// fallback to en-US
|
||||
labelStr = DateFormat.yMd('en-US').format(date);
|
||||
}
|
||||
}
|
||||
|
||||
return InlineActionsMenuItem(
|
||||
label: labelStr.capitalize(),
|
||||
|
||||
+31
-6
@@ -170,17 +170,32 @@ class ReminderReferenceService extends InlineActionsDelegate {
|
||||
final tomorrow = today.add(const Duration(days: 1));
|
||||
final oneWeek = today.add(const Duration(days: 7));
|
||||
|
||||
_allOptions = [
|
||||
_itemFromDate(
|
||||
late InlineActionsMenuItem todayItem;
|
||||
late InlineActionsMenuItem oneWeekItem;
|
||||
|
||||
try {
|
||||
todayItem = _itemFromDate(
|
||||
tomorrow,
|
||||
LocaleKeys.relativeDates_tomorrow.tr(),
|
||||
[DateFormat.yMd(_locale).format(tomorrow)],
|
||||
),
|
||||
_itemFromDate(
|
||||
);
|
||||
} catch (e) {
|
||||
todayItem = _itemFromDate(today);
|
||||
}
|
||||
|
||||
try {
|
||||
oneWeekItem = _itemFromDate(
|
||||
oneWeek,
|
||||
LocaleKeys.relativeDates_oneWeek.tr(),
|
||||
[DateFormat.yMd(_locale).format(oneWeek)],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
oneWeekItem = _itemFromDate(oneWeek);
|
||||
}
|
||||
|
||||
_allOptions = [
|
||||
todayItem,
|
||||
oneWeekItem,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -200,7 +215,17 @@ class ReminderReferenceService extends InlineActionsDelegate {
|
||||
String? label,
|
||||
List<String>? keywords,
|
||||
]) {
|
||||
final labelStr = label ?? DateFormat.yMd(_locale).format(date);
|
||||
late String labelStr;
|
||||
if (label != null) {
|
||||
labelStr = label;
|
||||
} else {
|
||||
try {
|
||||
labelStr = DateFormat.yMd(_locale).format(date);
|
||||
} catch (e) {
|
||||
// fallback to en-US
|
||||
labelStr = DateFormat.yMd('en-US').format(date);
|
||||
}
|
||||
}
|
||||
|
||||
return InlineActionsMenuItem(
|
||||
label: labelStr.capitalize(),
|
||||
|
||||
@@ -34,6 +34,7 @@ class FlowyEmojiSearchBar extends StatefulWidget {
|
||||
|
||||
class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -58,12 +59,18 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
||||
),
|
||||
const HSpace(8.0),
|
||||
_RandomEmojiButton(
|
||||
skinTone: skinTone,
|
||||
emojiData: widget.emojiData,
|
||||
onRandomEmojiSelected: widget.onRandomEmojiSelected,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
FlowyEmojiSkinToneSelector(
|
||||
onEmojiSkinToneChanged: widget.onSkinToneChanged,
|
||||
onEmojiSkinToneChanged: (v) {
|
||||
setState(() {
|
||||
skinTone = v;
|
||||
});
|
||||
widget.onSkinToneChanged.call(v);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -73,10 +80,12 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
||||
|
||||
class _RandomEmojiButton extends StatelessWidget {
|
||||
const _RandomEmojiButton({
|
||||
required this.skinTone,
|
||||
required this.emojiData,
|
||||
required this.onRandomEmojiSelected,
|
||||
});
|
||||
|
||||
final EmojiSkinTone skinTone;
|
||||
final EmojiData emojiData;
|
||||
final EmojiSelectedCallback onRandomEmojiSelected;
|
||||
|
||||
@@ -100,9 +109,14 @@ class _RandomEmojiButton extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
final random = emojiData.random;
|
||||
final emojiId = random.$1;
|
||||
final emoji = emojiData.getEmojiById(
|
||||
emojiId,
|
||||
skinTone: skinTone,
|
||||
);
|
||||
onRandomEmojiSelected(
|
||||
random.$1,
|
||||
random.$2,
|
||||
emojiId,
|
||||
emoji,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -131,6 +145,9 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
|
||||
/// this is to ensure that focus can be regained within a short period of time
|
||||
if (widget.ensureFocus) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (!mounted || focusNode.hasFocus) return;
|
||||
|
||||
+53
-11
@@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart' hide Icon;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
extension ToProto on FlowyIconType {
|
||||
@@ -46,6 +47,10 @@ enum FlowyIconType {
|
||||
custom;
|
||||
}
|
||||
|
||||
extension FlowyIconTypeToPickerTabType on FlowyIconType {
|
||||
PickerTabType? toPickerTabType() => name.toPickerTabType();
|
||||
}
|
||||
|
||||
class EmojiIconData {
|
||||
factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, '');
|
||||
|
||||
@@ -78,17 +83,35 @@ class EmojiIconData {
|
||||
bool get isNotEmpty => emoji.isNotEmpty;
|
||||
}
|
||||
|
||||
class SelectedEmojiIconResult {
|
||||
SelectedEmojiIconResult(this.data, this.keepOpen);
|
||||
|
||||
final EmojiIconData data;
|
||||
final bool keepOpen;
|
||||
|
||||
FlowyIconType get type => data.type;
|
||||
|
||||
String get emoji => data.emoji;
|
||||
}
|
||||
|
||||
extension EmojiIconDataToSelectedResultExtension on EmojiIconData {
|
||||
SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) =>
|
||||
SelectedEmojiIconResult(this, keepOpen);
|
||||
}
|
||||
|
||||
class FlowyIconEmojiPicker extends StatefulWidget {
|
||||
const FlowyIconEmojiPicker({
|
||||
super.key,
|
||||
this.onSelectedEmoji,
|
||||
this.initialType,
|
||||
this.enableBackgroundColorSelection = true,
|
||||
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
|
||||
});
|
||||
|
||||
final ValueChanged<EmojiIconData>? onSelectedEmoji;
|
||||
final ValueChanged<SelectedEmojiIconResult>? onSelectedEmoji;
|
||||
final bool enableBackgroundColorSelection;
|
||||
final List<PickerTabType> tabs;
|
||||
final PickerTabType? initialType;
|
||||
|
||||
@override
|
||||
State<FlowyIconEmojiPicker> createState() => _FlowyIconEmojiPickerState();
|
||||
@@ -96,12 +119,23 @@ class FlowyIconEmojiPicker extends StatefulWidget {
|
||||
|
||||
class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final controller = TabController(
|
||||
length: widget.tabs.length,
|
||||
vsync: this,
|
||||
);
|
||||
late TabController controller;
|
||||
int currentIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final initialType = widget.initialType;
|
||||
if (initialType != null) {
|
||||
currentIndex = widget.tabs.indexOf(initialType);
|
||||
}
|
||||
controller = TabController(
|
||||
initialIndex: currentIndex,
|
||||
length: widget.tabs.length,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
@@ -127,7 +161,8 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
),
|
||||
_RemoveIconButton(
|
||||
onTap: () {
|
||||
widget.onSelectedEmoji?.call(EmojiIconData.none());
|
||||
widget.onSelectedEmoji
|
||||
?.call(EmojiIconData.none().toSelectedResult());
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -155,9 +190,12 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
return FlowyEmojiPicker(
|
||||
ensureFocus: true,
|
||||
emojiPerLine: _getEmojiPerLine(context),
|
||||
onEmojiSelected: (_, emoji) => widget.onSelectedEmoji?.call(
|
||||
EmojiIconData.emoji(emoji),
|
||||
),
|
||||
onEmojiSelected: (r) {
|
||||
widget.onSelectedEmoji?.call(
|
||||
EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom),
|
||||
);
|
||||
SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,9 +209,13 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
|
||||
Widget _buildIconPicker() {
|
||||
return FlowyIconPicker(
|
||||
ensureFocus: true,
|
||||
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: (result) {
|
||||
widget.onSelectedEmoji?.call(result.toEmojiIconData());
|
||||
onSelectedIcon: (r) {
|
||||
widget.onSelectedEmoji?.call(
|
||||
r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom),
|
||||
);
|
||||
SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,8 +39,9 @@ class IconGroup {
|
||||
final filteredIcons = icons
|
||||
.where(
|
||||
(icon) =>
|
||||
icon.keywords.any((k) => k.contains(lowercaseKey)) ||
|
||||
icon.name.contains(lowercaseKey),
|
||||
icon.keywords
|
||||
.any((k) => k.toLowerCase().contains(lowercaseKey)) ||
|
||||
icon.name.toLowerCase().contains(lowercaseKey),
|
||||
)
|
||||
.toList();
|
||||
return IconGroup(name: name, icons: filteredIcons);
|
||||
@@ -84,3 +85,23 @@ class Icon {
|
||||
return '${iconGroup!.name}/$name';
|
||||
}
|
||||
}
|
||||
|
||||
class RecentIcon {
|
||||
factory RecentIcon.fromJson(Map<String, dynamic> json) =>
|
||||
RecentIcon(_$IconFromJson(json), json['groupName'] ?? '');
|
||||
|
||||
RecentIcon(this.icon, this.groupName);
|
||||
|
||||
final Icon icon;
|
||||
final String groupName;
|
||||
|
||||
String get name => icon.name;
|
||||
|
||||
List<String> get keywords => icon.keywords;
|
||||
|
||||
String get content => icon.content;
|
||||
|
||||
Map<String, dynamic> toJson() => _$IconToJson(
|
||||
Icon(name: name, keywords: keywords, content: content),
|
||||
)..addAll({'groupName': groupName});
|
||||
}
|
||||
@@ -75,17 +75,31 @@ Future<List<IconGroup>> loadIconGroups() async {
|
||||
}
|
||||
}
|
||||
|
||||
class IconPickerResult {
|
||||
IconPickerResult(this.data, this.isRandom);
|
||||
|
||||
final IconsData data;
|
||||
final bool isRandom;
|
||||
}
|
||||
|
||||
extension IconsDataToIconPickerResultExtension on IconsData {
|
||||
IconPickerResult toResult({bool isRandom = false}) =>
|
||||
IconPickerResult(this, isRandom);
|
||||
}
|
||||
|
||||
class FlowyIconPicker extends StatefulWidget {
|
||||
const FlowyIconPicker({
|
||||
super.key,
|
||||
required this.onSelectedIcon,
|
||||
required this.enableBackgroundColorSelection,
|
||||
this.iconPerLine = 9,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final bool enableBackgroundColorSelection;
|
||||
final ValueChanged<IconsData> onSelectedIcon;
|
||||
final ValueChanged<IconPickerResult> onSelectedIcon;
|
||||
final int iconPerLine;
|
||||
final bool ensureFocus;
|
||||
|
||||
@override
|
||||
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
|
||||
@@ -104,10 +118,13 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
||||
iconGroups.add(
|
||||
IconGroup(
|
||||
name: _kRecentIconGroupName,
|
||||
icons: recentIcons.sublist(
|
||||
0,
|
||||
min(recentIcons.length, widget.iconPerLine),
|
||||
),
|
||||
icons: recentIcons
|
||||
.sublist(
|
||||
0,
|
||||
min(recentIcons.length, widget.iconPerLine),
|
||||
)
|
||||
.map((e) => e.icon)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -142,6 +159,7 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: IconSearchBar(
|
||||
ensureFocus: widget.ensureFocus,
|
||||
onRandomTap: () {
|
||||
final value = kIconGroups?.randomIcon();
|
||||
if (value == null) {
|
||||
@@ -154,8 +172,9 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
||||
value.$2.content,
|
||||
value.$2.name,
|
||||
color,
|
||||
),
|
||||
).toResult(isRandom: true),
|
||||
);
|
||||
RecentIcons.putIcon(RecentIcon(value.$2, value.$1.name));
|
||||
},
|
||||
onKeywordChanged: (keyword) => {
|
||||
debounce.call(() {
|
||||
@@ -193,14 +212,14 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
||||
iconGroups: filteredIconGroups,
|
||||
enableBackgroundColorSelection:
|
||||
widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: widget.onSelectedIcon,
|
||||
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
|
||||
iconPerLine: widget.iconPerLine,
|
||||
);
|
||||
}
|
||||
return IconPicker(
|
||||
iconGroups: iconGroups,
|
||||
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: widget.onSelectedIcon,
|
||||
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
|
||||
iconPerLine: widget.iconPerLine,
|
||||
);
|
||||
},
|
||||
@@ -278,6 +297,7 @@ class _IconPickerState extends State<IconPicker> {
|
||||
crossAxisCount: widget.iconPerLine,
|
||||
),
|
||||
itemCount: iconGroup.icons.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final icon = iconGroup.icons[index];
|
||||
@@ -286,30 +306,38 @@ class _IconPickerState extends State<IconPicker> {
|
||||
icon: icon,
|
||||
mutex: mutex,
|
||||
onSelectedColor: (context, color) {
|
||||
String groupName = iconGroup.name;
|
||||
if (groupName == _kRecentIconGroupName) {
|
||||
groupName = getGroupName(index);
|
||||
}
|
||||
widget.onSelectedIcon(
|
||||
IconsData(
|
||||
iconGroup.name,
|
||||
groupName,
|
||||
icon.content,
|
||||
icon.name,
|
||||
color,
|
||||
),
|
||||
);
|
||||
RecentIcons.putIcon(icon);
|
||||
RecentIcons.putIcon(RecentIcon(icon, groupName));
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
)
|
||||
: _IconNoBackground(
|
||||
icon: icon,
|
||||
onSelectedIcon: () {
|
||||
String groupName = iconGroup.name;
|
||||
if (groupName == _kRecentIconGroupName) {
|
||||
groupName = getGroupName(index);
|
||||
}
|
||||
widget.onSelectedIcon(
|
||||
IconsData(
|
||||
iconGroup.name,
|
||||
groupName,
|
||||
icon.content,
|
||||
icon.name,
|
||||
null,
|
||||
),
|
||||
);
|
||||
RecentIcons.putIcon(icon);
|
||||
RecentIcons.putIcon(RecentIcon(icon, groupName));
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -324,6 +352,16 @@ class _IconPickerState extends State<IconPicker> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String getGroupName(int index) {
|
||||
final recentIcons = RecentIcons.getIconsSync();
|
||||
try {
|
||||
return recentIcons[index].groupName;
|
||||
} catch (e) {
|
||||
Log.error('getGroupName with index: $index error', e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _IconNoBackground extends StatelessWidget {
|
||||
@@ -375,12 +413,20 @@ class _Icon extends StatefulWidget {
|
||||
class _IconState extends State<_Icon> {
|
||||
final PopoverController _popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_popoverController.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
controller: _popoverController,
|
||||
offset: const Offset(0, 6),
|
||||
mutex: widget.mutex,
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
child: _IconNoBackground(
|
||||
icon: widget.icon,
|
||||
onSelectedIcon: () => _popoverController.show(),
|
||||
|
||||
@@ -16,9 +16,11 @@ class IconSearchBar extends StatefulWidget {
|
||||
super.key,
|
||||
required this.onRandomTap,
|
||||
required this.onKeywordChanged,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final VoidCallback onRandomTap;
|
||||
final bool ensureFocus;
|
||||
final IconKeywordChangedCallback onKeywordChanged;
|
||||
|
||||
@override
|
||||
@@ -46,6 +48,7 @@ class _IconSearchBarState extends State<IconSearchBar> {
|
||||
Expanded(
|
||||
child: _SearchTextField(
|
||||
onKeywordChanged: widget.onKeywordChanged,
|
||||
ensureFocus: widget.ensureFocus,
|
||||
),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
@@ -93,9 +96,11 @@ class _RandomIconButton extends StatelessWidget {
|
||||
class _SearchTextField extends StatefulWidget {
|
||||
const _SearchTextField({
|
||||
required this.onKeywordChanged,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final IconKeywordChangedCallback onKeywordChanged;
|
||||
final bool ensureFocus;
|
||||
|
||||
@override
|
||||
State<_SearchTextField> createState() => _SearchTextFieldState();
|
||||
@@ -105,6 +110,20 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
|
||||
/// this is to ensure that focus can be regained within a short period of time
|
||||
if (widget.ensureFocus) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (!mounted || focusNode.hasFocus) return;
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
||||
@@ -22,13 +22,10 @@ class RecentIcons {
|
||||
await _put(FlowyIconType.emoji, id);
|
||||
}
|
||||
|
||||
static Future<void> putIcon(Icon icon) async {
|
||||
static Future<void> putIcon(RecentIcon icon) async {
|
||||
await _put(
|
||||
FlowyIconType.icon,
|
||||
jsonEncode(
|
||||
Icon(name: icon.name, keywords: icon.keywords, content: icon.content)
|
||||
.toJson(),
|
||||
),
|
||||
jsonEncode(icon.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,12 +34,22 @@ class RecentIcons {
|
||||
return _dataMap[FlowyIconType.emoji.name] ?? [];
|
||||
}
|
||||
|
||||
static Future<List<Icon>> getIcons() async {
|
||||
static Future<List<RecentIcon>> getIcons() async {
|
||||
await _load();
|
||||
return getIconsSync();
|
||||
}
|
||||
|
||||
static List<RecentIcon> getIconsSync() {
|
||||
final iconList = _dataMap[FlowyIconType.icon.name] ?? [];
|
||||
try {
|
||||
return iconList
|
||||
.map((e) => Icon.fromJson(jsonDecode(e) as Map<String, dynamic>))
|
||||
.map(
|
||||
(e) => RecentIcon.fromJson(jsonDecode(e) as Map<String, dynamic>),
|
||||
)
|
||||
|
||||
/// skip the data that is already stored locally but has an empty
|
||||
/// groupName to accommodate the issue of destructive data modifications
|
||||
.skipWhile((e) => e.groupName.isEmpty)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
Log.error('RecentIcons getIcons with :$iconList', e);
|
||||
|
||||
@@ -15,6 +15,16 @@ enum PickerTabType {
|
||||
}
|
||||
}
|
||||
|
||||
extension StringToPickerTabType on String {
|
||||
PickerTabType? toPickerTabType() {
|
||||
try {
|
||||
return PickerTabType.values.byName(this);
|
||||
} on ArgumentError {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PickerTab extends StatelessWidget {
|
||||
const PickerTab({
|
||||
super.key,
|
||||
|
||||
@@ -29,6 +29,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/presentation.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -285,14 +286,26 @@ GoRoute _mobileEmojiPickerPageRoute() {
|
||||
state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle];
|
||||
final selectTabs =
|
||||
state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? '';
|
||||
final tabs = selectTabs
|
||||
.split('-')
|
||||
.map((e) => PickerTabType.values.byName(e))
|
||||
.toList();
|
||||
final selectedType = state
|
||||
.uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType]
|
||||
?.toPickerTabType();
|
||||
List<PickerTabType> tabs = [];
|
||||
try {
|
||||
tabs = selectTabs
|
||||
.split('-')
|
||||
.map((e) => PickerTabType.values.byName(e))
|
||||
.toList();
|
||||
} on ArgumentError catch (e) {
|
||||
Log.error('convert selectTabs to pickerTab error', e);
|
||||
}
|
||||
return MaterialExtendedPage(
|
||||
child: tabs.isEmpty
|
||||
? MobileEmojiPickerScreen(title: title)
|
||||
: MobileEmojiPickerScreen(title: title, tabs: tabs),
|
||||
? MobileEmojiPickerScreen(title: title, selectedType: selectedType)
|
||||
: MobileEmojiPickerScreen(
|
||||
title: title,
|
||||
selectedType: selectedType,
|
||||
tabs: tabs,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/user/application/user_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
|
||||
@@ -48,10 +49,17 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
emit(state.copyWith(isLoading: e.isLoading));
|
||||
},
|
||||
didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {
|
||||
// the latest view is shared across all the members of the workspace.
|
||||
|
||||
final latestView = value.setting.hasLatestView()
|
||||
? value.setting.latestView
|
||||
: state.latestView;
|
||||
|
||||
if (latestView != null && latestView.isSpace) {
|
||||
// If the latest view is a space, we don't need to open it.
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
workspaceSetting: value.setting,
|
||||
|
||||
+8
-1
@@ -8,7 +8,14 @@ const _friendlyFmt = 'MMM dd, y';
|
||||
const _dmyFmt = 'dd/MM/y';
|
||||
|
||||
extension DateFormatter on UserDateFormatPB {
|
||||
DateFormat get toFormat => DateFormat(_toFormat[this] ?? _friendlyFmt);
|
||||
DateFormat get toFormat {
|
||||
try {
|
||||
return DateFormat(_toFormat[this] ?? _friendlyFmt);
|
||||
} catch (_) {
|
||||
// fallback to en-US
|
||||
return DateFormat(_toFormat[this] ?? _friendlyFmt, 'en-US');
|
||||
}
|
||||
}
|
||||
|
||||
String formatDate(
|
||||
DateTime date,
|
||||
|
||||
Loaded 100 of 553 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user