Compare commits

...

21 commits
main ... 0.8.0

Author SHA1 Message Date
Richard Shiue
627b8ab43c fix(flutter_mobile): linked grid cannot be displayed in document (#7162) 2025-01-07 18:13:11 +08:00
Lucas.Xu
84d55b5022 fix: disable deleting mutilple nodes in table 2025-01-07 15:26:21 +08:00
Lucas.Xu
b53e20f746 chore: update editor version 2025-01-07 10:21:18 +08:00
Lucas.Xu
cd06161dea chore: upgrade andoird targetSdkVersion to 35 2025-01-07 09:39:39 +08:00
Morn
776f70a0c7 fix: icon picker issues on mobile (#7114)
* fix: icon picker issues on mobile (#7113)

* fix: error displaying in Page style

* fix: error displaying in Favorite/Recent page

* fix: complete the filter logic of icon picker

* fix: the color picker showed when tapping down

* fix: icons are not supported in subpage blocks

* chore: add some tests

* fix: recent icons not working for grid header icon

* fix: recent icon doesn't work in space icon (#7133)

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-01-07 09:38:59 +08:00
Lucas.Xu
1d1647d58d chore: bump version 0.8.0 2025-01-06 20:55:58 +08:00
Lucas
b3f94be33f fix: filter out the space when opening new tab (#7152)
* fix: filter out the space when opening tab

* test: filter out the space when opening tab
2025-01-06 17:11:18 +08:00
Lucas.Xu
ace398537b fix: outline block and link preview block padding on mobile 2025-01-06 17:09:27 +08:00
Lucas
430e21aec2 fix: simple table issues and locale issues (#7138)
* fix: can't make changes on row or column of table

* fix: fallback to en-US if the locale is invalid

* chore: remove unused code

* fix: simple table issues
2025-01-06 10:18:00 +08:00
Lucas
0c1eb7306a fix: convert false value in attributes to null (#7135) 2025-01-06 10:17:51 +08:00
Lucas
db11886e5f fix: simple table issues on mobile (#7115)
* fix: header row/column tap areas are too small on mobile

* test: header row/column tap areas are too small on mobile

* feat: enable auto scroll after inserting column or row

* fix: enter after emoji will create a softbreak on mobile

* fix: header row/column tap areas are too small on mobile

* fix: simple table alignment not work for item that wraps

* test: simple table alignment not work for item that wraps
2025-01-06 10:16:53 +08:00
Lucas
0d13336b32 fix: simple tests on mobile (#7102)
* fix: simple tests on mobile

* fix: subpage block padding
2025-01-06 10:16:43 +08:00
Richard Shiue
60ad397105 chore: bump dependencies (#7123)
* chore: bump dependencies

* test: fix unit test

* fix: downgrade percent indicator

* chore: flutter analyze
2025-01-03 10:11:19 +08:00
Lucas.Xu
fca3189c97 fix: subpage block padding 2024-12-30 21:28:06 +08:00
Lucas
e63f767926 feat: auto-dismiss collapsed handle on Android if no interaction occurs (#7088)
* feat: support auto-dismiss collapsed handle on Android

* fix: hit test area of collasepd handle is too big

* chore: upgrade appflowy_editor

* fix: simple table issues on mobile

* feat: highlight cell after insertion

* test: text color and cell background color test

* fix: sign_in_page_settings_test
2024-12-30 17:57:00 +08:00
Richard Shiue
8826e479eb fix(flutter_desktop): workspace menu ui issues (#7091)
* fix(flutter_desktop): remove log out and workspace option popovers conflict

* test: add integration test

* fix(flutter_desktop): workspace list scrollbar overlaps with list

* chore(flutter_desktop): fix padding around import from notion button

* chore(flutter_desktop): adjust popover conflict rules for workspace

* test: add integration tests

* chore(flutter_desktop): make the popoovers as barriers

* fix: regression from making the workspace item menu as barrier

* chore: update frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart

Co-authored-by: Lucas <lucas.xu@appflowy.io>

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2024-12-30 17:55:54 +08:00
Lucas
8e4fe3d559 chore: update changelog (#7095) 2024-12-30 17:54:30 +08:00
Richard Shiue
e2ee11e48a feat: add toast messages for ai chat interactions (#7086) 2024-12-30 17:20:23 +08:00
Morn
bdc0fa1f2a fix: toolbar menu not showing beacase keyboard height is not updated in time (#7060) 2024-12-30 17:20:15 +08:00
Morn
3836545682 fix: issues related to the emoji icon picker (#7063)
* fix: remove the scrolling conflict of the icon picker on macOS

* fix: the icon is not supported in sites tab

* feat: keep the icon panel open after click ramdom

* feat: the type of selector opened depends on the already set icon or emoji

* feat: the skin tone of the random emoji follows the selected skin ton

* fix: unit testing error
2024-12-30 17:20:08 +08:00
Richard Shiue
02eb0e0b83 chore(flutter_desktop): adjust toast style (#7083) 2024-12-30 17:19:56 +08:00
101 changed files with 2651 additions and 630 deletions

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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();
}

View file

@ -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);
});
}

View file

@ -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);
},
);
});

View file

@ -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 {

View file

@ -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 {

View file

@ -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);
});
});
}

View file

@ -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(

View file

@ -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) {

View file

@ -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 {

View file

@ -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,
);
});
});
}

View file

@ -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

View file

@ -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,

View file

@ -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 {

View file

@ -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);

View file

@ -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

View file

@ -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,
),

View file

@ -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(),
);

View file

@ -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(

View file

@ -274,7 +274,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
onPressed: () async {
final documentId = getOpenedDocumentId();
if (documentId != null) {
await onAddToExistingPage(documentId);
await onAddToExistingPage(context, documentId);
await forceReloadAndUpdateSelection(documentId);
} else {
widget.onOverrideVisibility?.call(true);
@ -298,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);
}
@ -309,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 {
@ -327,12 +334,43 @@ 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) {

View file

@ -90,6 +90,7 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
data == null) {
return _MetadataButton(
name: m.name,
onTap: () => widget.onSelectedMetadata?.call(m),
);
}
return BlocProvider(

View file

@ -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),

View file

@ -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) {

View file

@ -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);
},
),
);
},

View file

@ -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);
},
),
),

View file

@ -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,

View file

@ -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,

View file

@ -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,
);
}
}

View file

@ -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),

View file

@ -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;

View file

@ -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 {

View file

@ -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,10 +534,11 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
) {
return HeadingBlockComponentBuilder(
configuration: configuration.copyWith(
textStyle: (node) => _buildTextStyleInTableCell(
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
),
padding: (node) {
if (customHeadingPadding != null) {
@ -670,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);
},
),
);
}
@ -693,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),
@ -784,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) {
@ -823,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);
},
),
);
}
@ -836,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,
@ -872,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);
},
),
);
}
@ -881,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 ||
@ -895,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(

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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);
},
),
),

View file

@ -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);

View file

@ -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;

View file

@ -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: [

View file

@ -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)

View file

@ -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,

View file

@ -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);
},
),
),

View file

@ -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;
}
}

View file

@ -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(

View file

@ -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;

View file

@ -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');

View file

@ -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);
}
}
}

View file

@ -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();

View file

@ -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();

View file

@ -239,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,
),
),
],
@ -260,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,
),
),
],
@ -279,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) {
@ -291,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,
@ -330,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) {
@ -341,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,
),
@ -370,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,
),
);
}
@ -592,7 +623,7 @@ class _SimpleTableHeaderActionButtonState
child: CupertinoSwitch(
value: value,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (_) {},
onChanged: (_) => _toggle(),
),
),
);
@ -1198,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,

View file

@ -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() {

View file

@ -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;
},
);

View file

@ -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,

View file

@ -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,
);
}

View file

@ -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(),

View file

@ -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(),

View file

@ -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;

View file

@ -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');
},
);
}

View file

@ -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});
}

View file

@ -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(),

View file

@ -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();

View file

@ -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);

View file

@ -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,

View file

@ -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,
),
);
},
);

View file

@ -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,

View file

@ -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,

View file

@ -65,6 +65,10 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
state.currentPageManager.hideSecondaryPlugin();
emit(state.openPlugin(plugin: plugin, setLatest: setLatest));
if (setLatest) {
// the space view should be filtered out.
if (view != null && view.isSpace) {
return;
}
_setLatestOpenView(view);
}
},

View file

@ -2,9 +2,6 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -24,6 +21,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:time/time.dart';
@ -585,6 +584,10 @@ class PageManager {
value: _notifier,
child: Consumer<PageNotifier>(
builder: (_, notifier, __) {
if (notifier.plugin.pluginType == PluginType.blank) {
return const BlankPage();
}
return FadingIndexedStack(
index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
children: getIt<PluginSandbox>().supportPluginTypes.map(

View file

@ -173,21 +173,22 @@ class _SidebarSpaceHeaderState extends State<SidebarSpaceHeader> {
await _showRenameDialog();
break;
case SpaceMoreActionType.changeIcon:
final result = data as EmojiIconData;
if (data.type == FlowyIconType.icon) {
try {
final iconsData = IconsData.fromJson(jsonDecode(result.emoji));
context.read<SpaceBloc>().add(
SpaceEvent.changeIcon(
icon: '${iconsData.groupName}/${iconsData.iconName}',
iconColor: iconsData.color,
),
);
} on FormatException catch (e) {
context
.read<SpaceBloc>()
.add(const SpaceEvent.changeIcon(icon: ''));
Log.warn('SidebarSpaceHeader changeIcon error:$e');
if (data is SelectedEmojiIconResult) {
if (data.type == FlowyIconType.icon) {
try {
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
context.read<SpaceBloc>().add(
SpaceEvent.changeIcon(
icon: '${iconsData.groupName}/${iconsData.iconName}',
iconColor: iconsData.color,
),
);
} on FormatException catch (e) {
context
.read<SpaceBloc>()
.add(const SpaceEvent.changeIcon(icon: ''));
Log.warn('SidebarSpaceHeader changeIcon error:$e');
}
}
}
break;

View file

@ -18,15 +18,23 @@ enum WorkspaceMoreAction {
divider,
}
class WorkspaceMoreActionList extends StatelessWidget {
class WorkspaceMoreActionList extends StatefulWidget {
const WorkspaceMoreActionList({
super.key,
required this.workspace,
required this.isShowingMoreActions,
required this.popoverMutex,
});
final UserWorkspacePB workspace;
final ValueNotifier<bool> isShowingMoreActions;
final PopoverMutex popoverMutex;
@override
State<WorkspaceMoreActionList> createState() =>
_WorkspaceMoreActionListState();
}
class _WorkspaceMoreActionListState extends State<WorkspaceMoreActionList> {
bool isPopoverOpen = false;
@override
Widget build(BuildContext context) {
@ -45,16 +53,22 @@ class WorkspaceMoreActionList extends StatelessWidget {
return PopoverActionList<_WorkspaceMoreActionWrapper>(
direction: PopoverDirection.bottomWithLeftAligned,
actions: actions
.map((e) => _WorkspaceMoreActionWrapper(e, workspace))
.map(
(action) => _WorkspaceMoreActionWrapper(
action,
widget.workspace,
() => PopoverContainer.of(context).closeAll(),
),
)
.toList(),
mutex: widget.popoverMutex,
constraints: const BoxConstraints(minWidth: 220),
animationDuration: Durations.short3,
slideDistance: 2,
beginScaleFactor: 1.0,
beginOpacity: 0.8,
onClosed: () {
isShowingMoreActions.value = false;
},
onClosed: () => isPopoverOpen = false,
asBarrier: true,
buildChild: (controller) {
return SizedBox.square(
dimension: 24.0,
@ -64,11 +78,10 @@ class WorkspaceMoreActionList extends StatelessWidget {
FlowySvgs.workspace_three_dots_s,
),
onTap: () {
if (!isShowingMoreActions.value) {
if (!isPopoverOpen) {
controller.show();
isPopoverOpen = true;
}
isShowingMoreActions.value = true;
},
),
);
@ -79,10 +92,15 @@ class WorkspaceMoreActionList extends StatelessWidget {
}
class _WorkspaceMoreActionWrapper extends CustomActionCell {
_WorkspaceMoreActionWrapper(this.inner, this.workspace);
_WorkspaceMoreActionWrapper(
this.inner,
this.workspace,
this.closeWorkspaceMenu,
);
final WorkspaceMoreAction inner;
final UserWorkspacePB workspace;
final VoidCallback closeWorkspaceMenu;
@override
Widget buildWithContext(
@ -117,6 +135,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
margin: const EdgeInsets.all(6),
onTap: () async {
PopoverContainer.of(context).closeAll();
closeWorkspaceMenu();
final workspaceBloc = context.read<UserWorkspaceBloc>();
switch (inner) {

View file

@ -96,9 +96,9 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
margin: const EdgeInsets.all(0),
popupBuilder: (_) => FlowyIconEmojiPicker(
tabs: const [PickerTabType.emoji],
onSelectedEmoji: (result) {
widget.onSelected(result);
controller.close();
onSelectedEmoji: (r) {
widget.onSelected(r.data);
if (!r.keepOpen) controller.close();
},
),
child: MouseRegion(

View file

@ -43,13 +43,7 @@ class WorkspacesMenu extends StatefulWidget {
}
class _WorkspacesMenuState extends State<WorkspacesMenu> {
final ValueNotifier<bool> isShowingMoreActions = ValueNotifier(false);
@override
void dispose() {
isShowingMoreActions.dispose();
super.dispose();
}
final popoverMutex = PopoverMutex();
@override
Widget build(BuildContext context) {
@ -59,7 +53,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
children: [
// user email
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0),
child: Row(
children: [
Expanded(
@ -71,18 +65,21 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
),
),
const HSpace(4.0),
const _WorkspaceMoreButton(),
WorkspaceMoreButton(
popoverMutex: popoverMutex,
),
const HSpace(8.0),
],
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0),
child: Divider(height: 1.0),
),
// workspace list
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -93,7 +90,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
userProfile: widget.userProfile,
isSelected: workspace.workspaceId ==
widget.currentWorkspace.workspaceId,
isShowingMoreActions: isShowingMoreActions,
popoverMutex: popoverMutex,
),
const VSpace(6.0),
],
@ -102,13 +99,19 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
),
),
// add new workspace
const _CreateWorkspaceButton(),
const VSpace(6.0),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 6.0),
child: _CreateWorkspaceButton(),
),
if (UniversalPlatform.isDesktop) ...[
const _ImportNotionButton(),
const VSpace(6.0),
const Padding(
padding: EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0),
child: _ImportNotionButton(),
),
],
const VSpace(6.0),
],
);
}
@ -132,13 +135,13 @@ class WorkspaceMenuItem extends StatefulWidget {
required this.workspace,
required this.userProfile,
required this.isSelected,
required this.isShowingMoreActions,
required this.popoverMutex,
});
final UserProfilePB userProfile;
final UserWorkspacePB workspace;
final bool isSelected;
final ValueNotifier<bool> isShowingMoreActions;
final PopoverMutex popoverMutex;
@override
State<WorkspaceMenuItem> createState() => _WorkspaceMenuItemState();
@ -230,7 +233,7 @@ class _WorkspaceMenuItemState extends State<WorkspaceMenuItem> {
},
child: WorkspaceMoreActionList(
workspace: widget.workspace,
isShowingMoreActions: widget.isShowingMoreActions,
popoverMutex: widget.popoverMutex,
),
),
const HSpace(8.0),
@ -394,40 +397,35 @@ class _ImportNotionButton extends StatelessWidget {
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Stack(
alignment: Alignment.centerRight,
children: [
FlowyButton(
key: importNotionButtonKey,
onTap: () {
_showImportNotinoDialog(context);
child: FlowyButton(
key: importNotionButtonKey,
onTap: () {
_showImportNotinoDialog(context);
},
margin: const EdgeInsets.symmetric(horizontal: 4.0),
text: Row(
children: [
_buildLeftIcon(context),
const HSpace(8.0),
FlowyText.regular(
LocaleKeys.workspace_importFromNotion.tr(),
),
],
),
rightIcon: FlowyTooltip(
message: LocaleKeys.workspace_learnMore.tr(),
preferBelow: true,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.information_s,
),
onPressed: () {
afLaunchUrlString(
'https://docs.appflowy.io/docs/guides/import-from-notion',
);
},
margin: const EdgeInsets.symmetric(horizontal: 4.0),
text: Row(
children: [
_buildLeftIcon(context),
const HSpace(8.0),
FlowyText.regular(
LocaleKeys.workspace_importFromNotion.tr(),
),
],
),
),
FlowyTooltip(
message: LocaleKeys.workspace_learnMore.tr(),
preferBelow: true,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.information_s,
),
onPressed: () {
afLaunchUrlString(
'https://docs.appflowy.io/docs/guides/import-from-notion',
);
},
),
),
],
),
),
);
}
@ -478,14 +476,22 @@ class _ImportNotionButton extends StatelessWidget {
}
}
class _WorkspaceMoreButton extends StatelessWidget {
const _WorkspaceMoreButton();
@visibleForTesting
class WorkspaceMoreButton extends StatelessWidget {
const WorkspaceMoreButton({
super.key,
required this.popoverMutex,
});
final PopoverMutex popoverMutex;
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 6),
mutex: popoverMutex,
asBarrier: true,
popupBuilder: (_) => FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0),
leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s),

View file

@ -207,6 +207,7 @@ class _SidebarSwitchWorkspaceButtonState
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 5),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
margin: EdgeInsets.zero,
animationDuration: Durations.short3,
beginScaleFactor: 1.0,
beginOpacity: 0.8,

View file

@ -641,12 +641,13 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
popupBuilder: (context) {
isIconPickerOpened = true;
return FlowyIconEmojiPicker(
onSelectedEmoji: (result) {
initialType: iconData.type.toPickerTabType(),
onSelectedEmoji: (r) {
ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: result,
viewIcon: r.data,
);
controller.close();
if (!r.keepOpen) controller.close();
},
);
},
@ -770,13 +771,12 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());
break;
case ViewMoreActionType.changeIcon:
if (data is! EmojiIconData) {
if (data is! SelectedEmojiIconResult) {
return;
}
final result = data;
await ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: result,
viewIcon: data.data,
);
break;
case ViewMoreActionType.moveTo:

View file

@ -58,7 +58,11 @@ class ViewMoreActionPopover extends StatelessWidget {
(e) => ViewMoreActionTypeWrapper(e, view, (controller, data) {
onEditing(false);
onAction(e, data);
controller.close();
bool enableClose = true;
if (data is SelectedEmojiIconResult) {
if (data.keepOpen) enableClose = false;
}
if (enableClose) controller.close();
}),
)
.toList();
@ -172,6 +176,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
margin: const EdgeInsets.all(0),
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (_) => FlowyIconEmojiPicker(
initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(),
onSelectedEmoji: (result) => onTap(controller, result),
),
child: child,

View file

@ -1,4 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@ -51,13 +53,9 @@ class PublishInfoViewItem extends StatelessWidget {
}
Widget _buildIcon() {
final icon = publishInfoView.view.icon.value;
final icon = publishInfoView.view.icon.toEmojiIconData();
return icon.isNotEmpty
? FlowyText.emoji(
icon,
fontSize: 16.0,
figmaLineHeight: 18.0,
)
? RawEmojiIconWidget(emoji: icon, emojiSize: 16.0)
: publishInfoView.view.defaultIcon();
}
}

View file

@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
SelectionMenuItem emojiMenuItem = SelectionMenuItem(
getName: LocaleKeys.document_plugins_emoji.tr,
@ -109,7 +108,7 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
@override
Widget build(BuildContext context) {
return FlowyEmojiPicker(
onEmojiSelected: (_, emoji) => widget.onSubmitted(emoji),
onEmojiSelected: (r) => widget.onSubmitted(r.emoji),
);
}
}

View file

@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
@ -364,71 +365,59 @@ class OkCancelButton extends StatelessWidget {
void showToastNotification(
BuildContext context, {
required String message,
String? message,
TextSpan? richMessage,
String? description,
ToastificationType type = ToastificationType.success,
ToastificationCallbacks? callbacks,
double bottomPadding = 100,
}) {
if (UniversalPlatform.isMobile) {
toastification.showCustom(
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(milliseconds: 3000),
callbacks: callbacks ?? const ToastificationCallbacks(),
builder: (_, __) => _MToast(
message: message,
type: type,
bottomPadding: bottomPadding,
description: description,
),
);
return;
}
toastification.show(
context: context,
type: type,
style: ToastificationStyle.flat,
closeButtonShowType: CloseButtonShowType.onHover,
assert(
(message == null) != (richMessage == null),
"Exactly one of message or richMessage must be non-null.",
);
toastification.showCustom(
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(milliseconds: 3000),
showProgressBar: false,
backgroundColor: Theme.of(context).colorScheme.surface,
borderSide: BorderSide(
color: Colors.grey.withOpacity(0.4),
),
title: FlowyText(
message,
maxLines: 3,
),
description: description != null
? FlowyText.regular(
description,
fontSize: 12,
lineHeight: 1.2,
maxLines: 3,
)
: null,
callbacks: callbacks ?? const ToastificationCallbacks(),
builder: (_, item) {
return UniversalPlatform.isMobile
? _MobileToast(
message: message,
type: type,
bottomPadding: bottomPadding,
description: description,
)
: _DesktopToast(
message: message,
richMessage: richMessage,
type: type,
onDismiss: () => toastification.dismiss(item),
);
},
);
}
class _MToast extends StatelessWidget {
const _MToast({
required this.message,
class _MobileToast extends StatelessWidget {
const _MobileToast({
this.message,
this.type = ToastificationType.success,
this.bottomPadding = 100,
this.description,
});
final String message;
final String? message;
final ToastificationType type;
final double bottomPadding;
final String? description;
@override
Widget build(BuildContext context) {
if (message == null) {
return const SizedBox.shrink();
}
final hintText = FlowyText.regular(
message,
message!,
fontSize: 16.0,
figmaLineHeight: 18.0,
color: Colors.white,
@ -498,6 +487,90 @@ class _MToast extends StatelessWidget {
}
}
class _DesktopToast extends StatelessWidget {
const _DesktopToast({
this.message,
this.richMessage,
required this.type,
this.onDismiss,
});
final String? message;
final TextSpan? richMessage;
final ToastificationType type;
final void Function()? onDismiss;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 360.0),
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
margin: const EdgeInsets.only(bottom: 32.0),
decoration: BoxDecoration(
color: Theme.of(context).isLightMode
? const Color(0xFF333333)
: const Color(0xFF363D49),
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// icon
FlowySvg(
switch (type) {
ToastificationType.warning => FlowySvgs.toast_warning_filled_s,
ToastificationType.success => FlowySvgs.toast_checked_filled_s,
ToastificationType.error => FlowySvgs.toast_error_filled_s,
_ => throw UnimplementedError(),
},
size: const Size.square(20.0),
blendMode: null,
),
const HSpace(8.0),
// text
Flexible(
child: message != null
? FlowyText(
message!,
maxLines: 2,
figmaLineHeight: 20.0,
overflow: TextOverflow.ellipsis,
color: const Color(0xFFFFFFFF),
)
: RichText(
text: richMessage!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(16.0),
// close
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onDismiss,
child: const SizedBox.square(
dimension: 24.0,
child: Center(
child: FlowySvg(
FlowySvgs.toast_close_s,
size: Size.square(16.0),
color: Color(0xFFBDBDBD),
),
),
),
),
),
],
),
),
);
}
}
Future<void> showConfirmDeletionDialog({
required BuildContext context,
required String name,

View file

@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';
/// This value is used to disable the auto scroll when dragging.
///
/// It is used to prevent the auto scroll when dragging a view item to a document.
bool disableAutoScrollWhenDragging = false;
class DraggableItem<T extends Object> extends StatefulWidget {
const DraggableItem({
super.key,
@ -67,7 +72,7 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
childWhenDragging: widget.childWhenDragging ?? widget.child,
child: widget.child,
onDragUpdate: (details) {
if (widget.enableAutoScroll) {
if (widget.enableAutoScroll && !disableAutoScrollWhenDragging) {
dragTarget = details.globalPosition & widget.hitTestSize;
autoScroller?.startAutoScrollIfNecessary(dragTarget!);
}
@ -88,7 +93,7 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
}
void initAutoScrollerIfNeeded(BuildContext context) {
if (!widget.enableAutoScroll) {
if (!widget.enableAutoScroll || disableAutoScrollWhenDragging) {
return;
}
@ -104,7 +109,7 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
autoScroller = EdgeDraggingAutoScroller(
scrollable!,
onScrollViewScrolled: () {
if (dragTarget != null) {
if (dragTarget != null && !disableAutoScrollWhenDragging) {
autoScroller!.startAutoScrollIfNecessary(dragTarget!);
}
},

View file

@ -91,13 +91,15 @@ class _RenameViewPopoverState extends State<RenameViewPopover> {
}
Future<void> _updateViewIcon(
EmojiIconData emoji,
SelectedEmojiIconResult r,
PopoverController? _,
) async {
await ViewBackendService.updateViewIcon(
viewId: widget.viewId,
viewIcon: emoji,
viewIcon: r.data,
);
widget.popoverController.close();
if (!r.keepOpen) {
widget.popoverController.close();
}
}
}

View file

@ -136,24 +136,24 @@ SPEC CHECKSUMS:
connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6
hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c
irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823

View file

@ -29,10 +29,10 @@ packages:
dependency: "direct main"
description:
name: any_date
sha256: "3981efcc15edd1673bcfc1aec298cc6079029fbffb3734c7eae8ceeb878f911e"
sha256: e9ed245ba44ccebf3c2d6daa3592213f409821128593d448b219a1f8e9bd17a1
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.1.1"
app_links:
dependency: "direct main"
description:
@ -61,8 +61,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: c68e5f6
resolved-ref: c68e5f6c585205083e27e875b822656425b2853f
ref: "4bcbfb0"
resolved-ref: "4bcbfb0679d07d9d4010869ea4bc2f2b7e32c479"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "4.0.0"
@ -70,8 +70,8 @@ packages:
dependency: "direct main"
description:
path: "packages/appflowy_editor_plugins"
ref: "8047c21"
resolved-ref: "8047c21868273d544684522eb61e4ac2d2041409"
ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb
resolved-ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb
url: "https://github.com/AppFlowy-IO/AppFlowy-plugins.git"
source: git
version: "0.0.6"
@ -101,10 +101,10 @@ packages:
dependency: transitive
description:
name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.6.0"
async:
dependency: transitive
description:
@ -269,10 +269,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
url: "https://pub.dev"
source: hosted
version: "8.9.2"
version: "8.9.3"
cached_network_image:
dependency: "direct main"
description:
@ -374,26 +374,26 @@ packages:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.9.2"
version: "1.11.1"
cross_cache:
dependency: transitive
description:
name: cross_cache
sha256: ed30348320a7fefe4195c26cfcbabc76b7108ce3d364c4dd7c1b1c681a4cfe28
sha256: "3879d1661f211e89d81ece419684df5111b5a611aa6200cd405e8332031765e9"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
version: "0.0.3"
cross_file:
dependency: "direct main"
description:
@ -406,18 +406,18 @@ packages:
dependency: transitive
description:
name: crypto
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.2"
dart_style:
dependency: transitive
description:
@ -462,10 +462,10 @@ packages:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba"
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
version: "7.0.2"
diff_match_patch:
dependency: transitive
description:
@ -550,10 +550,10 @@ packages:
dependency: "direct main"
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.5"
version: "2.0.7"
expandable:
dependency: "direct main"
description:
@ -566,18 +566,18 @@ packages:
dependency: "direct main"
description:
name: extended_text_field
sha256: "954c7eea1e82728a742f7ddf09b9a51cef087d4f52b716ba88cb3eb78ccd7c6e"
sha256: fb5c35460a54906a0ada2a88a968cdfc71d71aebbaf9022debb5d67f47748964
url: "https://pub.dev"
source: hosted
version: "15.0.0"
version: "15.0.1"
extended_text_library:
dependency: "direct main"
description:
name: extended_text_library
sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829"
sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba"
url: "https://pub.dev"
source: hosted
version: "12.0.0"
version: "12.0.1"
fake_async:
dependency: transitive
description:
@ -606,26 +606,26 @@ packages:
dependency: transitive
description:
name: file_picker
sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12"
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
url: "https://pub.dev"
source: hosted
version: "8.1.2"
version: "8.1.7"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
source: hosted
version: "0.9.2+1"
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
version: "0.9.4+2"
file_selector_platform_interface:
dependency: transitive
description:
@ -638,34 +638,34 @@ packages:
dependency: transitive
description:
name: file_selector_windows
sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69"
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
version: "0.9.3+3"
fixnum:
dependency: "direct main"
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
flex_color_picker:
dependency: "direct main"
description:
name: flex_color_picker
sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e"
sha256: "12dc855ae8ef5491f529b1fc52c655f06dcdf4114f1f7fdecafa41eec2ec8d79"
url: "https://pub.dev"
source: hosted
version: "3.5.1"
version: "3.6.0"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: "7d97ba5c20f0e5cb1e3e2c17c865e1f797d129de31fc1f75d2dcce9470d6373c"
sha256: "7639d2c86268eff84a909026eb169f008064af0fb3696a651b24b0fa24a40334"
url: "https://pub.dev"
source: hosted
version: "3.3.0"
version: "3.4.1"
flowy_infra:
dependency: "direct main"
description:
@ -703,10 +703,10 @@ packages:
dependency: "direct main"
description:
name: flutter_animate
sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5"
sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5"
url: "https://pub.dev"
source: hosted
version: "4.5.0"
version: "4.5.2"
flutter_bloc:
dependency: "direct main"
description:
@ -757,8 +757,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "8a9fa49"
resolved-ref: "8a9fa491cb3b86baf78b0a33c2c37a29d1cae028"
ref: "355aa56"
resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8"
url: "https://github.com/LucasXu0/emoji_mart.git"
source: git
version: "1.0.2"
@ -819,18 +819,18 @@ packages:
dependency: transitive
description:
name: flutter_shaders
sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe"
sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
version: "0.1.3"
flutter_slidable:
dependency: "direct main"
description:
name: flutter_slidable
sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3"
sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
flutter_staggered_grid_view:
dependency: "direct main"
description:
@ -851,10 +851,10 @@ packages:
dependency: transitive
description:
name: flutter_svg
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
version: "2.0.16"
flutter_test:
dependency: "direct dev"
description: flutter
@ -877,10 +877,10 @@ packages:
dependency: "direct main"
description:
name: fluttertoast
sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc"
sha256: "24467dc20bbe49fd63e57d8e190798c4d22cbbdac30e54209d153a15273721d1"
url: "https://pub.dev"
source: hosted
version: "8.2.8"
version: "8.2.10"
freezed:
dependency: "direct dev"
description:
@ -930,10 +930,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539"
url: "https://pub.dev"
source: hosted
version: "14.2.7"
version: "14.6.2"
google_fonts:
dependency: "direct main"
description:
@ -994,10 +994,10 @@ packages:
dependency: transitive
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
version: "0.15.5"
http:
dependency: "direct main"
description:
@ -1010,10 +1010,10 @@ packages:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
http_parser:
dependency: transitive
description:
@ -1058,18 +1058,18 @@ packages:
dependency: transitive
description:
name: image_picker_for_web
sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50"
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.6"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447"
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
url: "https://pub.dev"
source: hosted
version: "0.8.12"
version: "0.8.12+1"
image_picker_linux:
dependency: transitive
description:
@ -1119,10 +1119,10 @@ packages:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.0.5"
irondash_engine_context:
dependency: transitive
description:
@ -1255,18 +1255,18 @@ packages:
dependency: transitive
description:
name: logger
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.5.0"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
markdown:
dependency: "direct main"
description:
@ -1367,10 +1367,10 @@ packages:
dependency: "direct main"
description:
name: numerus
sha256: "49cd96fe774dd1f574fc9117ed67e8a2b06a612f723e87ef3119456a7729d837"
sha256: a17a3f34527497e89378696a76f382b40dc534c4a57b3778de246ebc1ce2ca99
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.0"
octo_image:
dependency: transitive
description:
@ -1383,34 +1383,34 @@ packages:
dependency: "direct main"
description:
name: open_filex
sha256: ba425ea49affd0a98a234aa9344b9ea5d4c4f7625a1377961eae9fe194c3d523
sha256: dcb7bd3d32db8db5260253a62f1564c02c2c8df64bc0187cd213f65f827519bd
url: "https://pub.dev"
source: hosted
version: "4.5.0"
version: "4.6.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918
sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d"
url: "https://pub.dev"
source: hosted
version: "8.0.2"
version: "8.1.2"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66
sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
path:
dependency: "direct main"
description:
@ -1431,18 +1431,18 @@ packages:
dependency: transitive
description:
name: path_parsing
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.1.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
@ -1455,10 +1455,10 @@ packages:
dependency: transitive
description:
name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
@ -1519,10 +1519,10 @@ packages:
dependency: transitive
description:
name: permission_handler_android
sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
url: "https://pub.dev"
source: hosted
version: "12.0.12"
version: "12.0.13"
permission_handler_apple:
dependency: transitive
description:
@ -1535,10 +1535,10 @@ packages:
dependency: transitive
description:
name: permission_handler_html
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+2"
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
@ -1623,18 +1623,18 @@ packages:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
qr:
dependency: transitive
description:
@ -1735,26 +1735,26 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52"
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
url: "https://pub.dev"
source: hosted
version: "10.0.2"
version: "10.1.3"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5"
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "5.0.2"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.3"
shared_preferences_android:
dependency: transitive
description:
@ -1767,10 +1767,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.2"
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
@ -1885,10 +1885,10 @@ packages:
dependency: transitive
description:
name: source_helper
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.4"
version: "1.3.5"
source_map_stack_trace:
dependency: transitive
description:
@ -1901,10 +1901,10 @@ packages:
dependency: transitive
description:
name: source_maps
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.12"
version: "0.10.13"
source_span:
dependency: transitive
description:
@ -1957,10 +1957,10 @@ packages:
dependency: transitive
description:
name: stream_transform
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -2029,10 +2029,10 @@ packages:
dependency: "direct main"
description:
name: table_calendar
sha256: "4ca32b2fc919452c9974abd4c6ea611a63e33b9e4f0b8c38dba3ac1f4a6549d1"
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
term_glyph:
dependency: transitive
description:
@ -2069,26 +2069,26 @@ packages:
dependency: "direct main"
description:
name: time
sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221
sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
timing:
dependency: transitive
description:
name: timing
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.0.2"
toastification:
dependency: "direct main"
description:
name: toastification
sha256: "441adf261f03b82db7067cba349756f70e9e2c0b7276bcba856210742f85f394"
sha256: "4d97fbfa463dfe83691044cba9f37cb185a79bb9205cfecb655fa1f6be126a13"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.0"
tuple:
dependency: transitive
description:
@ -2141,10 +2141,10 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
@ -2157,26 +2157,26 @@ packages:
dependency: transitive
description:
name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
version: "6.3.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.2"
url_launcher_platform_interface:
dependency: "direct dev"
description:
@ -2197,10 +2197,10 @@ packages:
dependency: transitive
description:
name: url_launcher_windows
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
url_protocol:
dependency: "direct main"
description:
@ -2214,10 +2214,10 @@ packages:
dependency: "direct overridden"
description:
name: uuid
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.0"
version: "4.5.1"
value_layout_builder:
dependency: transitive
description:
@ -2230,26 +2230,26 @@ packages:
dependency: transitive
description:
name: vector_graphics
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
version: "1.1.15"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
version: "1.1.12"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
version: "1.1.16"
vector_math:
dependency: transitive
description:
@ -2278,18 +2278,18 @@ packages:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
web:
dependency: transitive
description:
name: web
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.0"
web_socket_channel:
dependency: transitive
description:
@ -2366,10 +2366,10 @@ packages:
dependency: transitive
description:
name: win32_registry
sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6"
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
version: "1.1.5"
window_manager:
dependency: "direct main"
description:
@ -2382,10 +2382,10 @@ packages:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.1.0"
xml:
dependency: transitive
description:
@ -2398,10 +2398,10 @@ packages:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"

View file

@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an
your data. The best open source alternative to Notion.
publish_to: "none"
version: 0.7.9
version: 0.8.0
environment:
flutter: ">=3.22.0"
@ -73,7 +73,7 @@ dependencies:
flutter_emoji_mart:
git:
url: https://github.com/LucasXu0/emoji_mart.git
ref: "8a9fa49"
ref: "355aa56"
flutter_math_fork: ^0.7.3
flutter_slidable: ^3.0.0
@ -174,13 +174,13 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "c68e5f6"
ref: "4bcbfb0"
appflowy_editor_plugins:
git:
url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git
path: "packages/appflowy_editor_plugins"
ref: "8047c21"
ref: "ca8289099e40e0d6ad0605fbbe01fde3091538bb"
sheet:
git:

View file

@ -109,7 +109,8 @@ void main() {
assert(boardBloc.groupControllers.values.length == 2);
assert(
boardBloc.boardController.groupDatas.last.headerData.groupName == "2024",
boardBloc.boardController.groupDatas.last.headerData.groupName ==
DateTime.now().year.toString(),
);
});
}

View file

@ -290,5 +290,107 @@ void main() {
await editorState.apply(transaction);
await completer.future;
});
test('text retain with attributes that are false', () async {
final node = paragraphNode(
delta: Delta()
..insert(
'Hello AppFlowy',
attributes: {
'bold': true,
},
),
);
final document = Document(
root: pageNode(
children: [
node,
],
),
);
final transactionAdapter = TransactionAdapter(
documentId: '',
documentService: DocumentService(),
);
final editorState = EditorState(
document: document,
);
int counter = 0;
final completer = Completer();
editorState.transactionStream.listen((event) {
final time = event.$1;
if (time == TransactionTime.before) {
final actions = transactionAdapter.transactionToBlockActions(
event.$2,
editorState,
);
final textActions =
transactionAdapter.filterTextDeltaActions(actions);
final blockActions = transactionAdapter.filterBlockActions(actions);
expect(textActions.length, 1);
expect(blockActions.length, 1);
if (counter == 1) {
// check text operation
final textAction = textActions.first;
final textId = textAction.textDeltaPayloadPB?.textId;
{
expect(textAction.textDeltaType, TextDeltaType.create);
expect(textId, isNotEmpty);
final delta = textAction.textDeltaPayloadPB?.delta;
expect(
delta,
equals(
'[{"insert":"Hello","attributes":{"bold":null}},{"insert":" AppFlowy","attributes":{"bold":true}}]',
),
);
}
} else if (counter == 3) {
final textAction = textActions.first;
final textId = textAction.textDeltaPayloadPB?.textId;
{
expect(textAction.textDeltaType, TextDeltaType.update);
expect(textId, isNotEmpty);
final delta = textAction.textDeltaPayloadPB?.delta;
expect(
delta,
equals(
'[{"retain":5,"attributes":{"bold":null}}]',
),
);
}
}
} else if (time == TransactionTime.after && counter == 3) {
completer.complete();
}
});
counter = 1;
final insertTransaction = editorState.transaction;
insertTransaction.formatText(node, 0, 5, {
'bold': false,
});
await editorState.apply(insertTransaction);
counter = 2;
final updateTransaction = editorState.transaction;
updateTransaction.formatText(node, 0, 5, {
'bold': true,
});
await editorState.apply(updateTransaction);
counter = 3;
final formatTransaction = editorState.transaction;
formatTransaction.formatText(node, 0, 5, {
'bold': false,
});
await editorState.apply(formatTransaction);
await completer.future;
});
});
}

View file

@ -170,5 +170,67 @@ void main() {
'0': TableAlign.center.key,
});
});
test('delete a column with text color & bold style (1)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 3,
columnCount: 4,
);
// delete the column 1
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
await editorState.deleteColumnInTable(tableNode, 0);
expect(tableNode.columnTextColors, {
'0': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'0': true,
});
expect(tableNode.rowLength, 3);
expect(tableNode.columnLength, 3);
});
test('delete a column with text color & bold style (2)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 3,
columnCount: 4,
);
// delete the column 1
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
await editorState.deleteColumnInTable(tableNode, 1);
expect(tableNode.columnTextColors, {});
expect(tableNode.columnBoldAttributes, {});
expect(tableNode.rowLength, 3);
expect(tableNode.columnLength, 3);
});
});
}

View file

@ -161,5 +161,69 @@ void main() {
'1': TableAlign.center.key,
});
});
test('duplicate a column with text color & bold style (1)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 3,
columnCount: 4,
);
// duplicate the column 1
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
await editorState.duplicateColumnInTable(tableNode, 1);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
'2': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
'2': true,
});
});
test('duplicate a column with text color & bold style (2)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 3,
columnCount: 4,
);
// duplicate the column 1
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
await editorState.duplicateColumnInTable(tableNode, 0);
expect(tableNode.columnTextColors, {
'2': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'2': true,
});
});
});
}

View file

@ -190,5 +190,67 @@ void main() {
'0': TableAlign.center.key,
});
});
test('insert a column with text color & bold style (1)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 2,
columnCount: 3,
);
// insert the column at the first position
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'0': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'0': true,
});
await editorState.insertColumnInTable(tableNode, 0);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
});
test('insert a column with text color & bold style (2)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 2,
columnCount: 3,
);
// insert the column at the first position
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'0': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'0': true,
});
await editorState.insertColumnInTable(tableNode, 1);
expect(tableNode.columnTextColors, {
'0': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'0': true,
});
});
});
}

View file

@ -1,5 +1,6 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'simple_table_test_helper.dart';
@ -193,5 +194,45 @@ void main() {
expect(tableNode.tableAlign, align);
}
});
test('clear the existing align of the column before updating', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 2,
columnCount: 3,
);
final firstCellNode = tableNode.getTableCellNode(
rowIndex: 0,
columnIndex: 0,
);
Node firstParagraphNode = firstCellNode!.children.first;
// format the first paragraph to center align
final transaction = editorState.transaction;
transaction.updateNode(
firstParagraphNode,
{
blockComponentAlign: TableAlign.right.key,
},
);
await editorState.apply(transaction);
firstParagraphNode = editorState.getNodeAtPath([0, 0, 0, 0])!;
expect(
firstParagraphNode.attributes[blockComponentAlign],
TableAlign.right.key,
);
await editorState.updateColumnAlign(
tableCellNode: firstCellNode,
align: TableAlign.center,
);
expect(
firstParagraphNode.attributes[blockComponentAlign],
null,
);
});
});
}

View file

@ -46,16 +46,17 @@ void main() {
});
test('putIcons', () async {
List<Icon> icons = await RecentIcons.getIcons();
List<RecentIcon> icons = await RecentIcons.getIcons();
assert(icons.isEmpty);
await loadIconGroups();
final groups = kIconGroups!;
final List<Icon> localIcons = [];
final List<RecentIcon> localIcons = [];
for (final e in groups) {
localIcons.addAll(e.icons);
localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());
}
bool equalIcon(Icon a, Icon b) =>
bool equalIcon(RecentIcon a, RecentIcon b) =>
a.groupName == b.groupName &&
a.name == b.name &&
a.keywords.equals(b.keywords) &&
a.content == b.content;

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6 13.8L8.45 11.65C8.26667 11.4667 8.03333 11.375 7.75 11.375C7.46667 11.375 7.23333 11.4667 7.05 11.65C6.86667 11.8333 6.775 12.0667 6.775 12.35C6.775 12.6333 6.86667 12.8667 7.05 13.05L9.9 15.9C10.1 16.1 10.3333 16.2 10.6 16.2C10.8667 16.2 11.1 16.1 11.3 15.9L16.95 10.25C17.1333 10.0667 17.225 9.83333 17.225 9.55C17.225 9.26667 17.1333 9.03333 16.95 8.85C16.7667 8.66667 16.5333 8.575 16.25 8.575C15.9667 8.575 15.7333 8.66667 15.55 8.85L10.6 13.8ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#3AC25C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 724 B

After

Width:  |  Height:  |  Size: 724 B

Before After
Before After

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 17C12.2833 17 12.5208 16.9042 12.7125 16.7125C12.9042 16.5208 13 16.2833 13 16C13 15.7167 12.9042 15.4792 12.7125 15.2875C12.5208 15.0958 12.2833 15 12 15C11.7167 15 11.4792 15.0958 11.2875 15.2875C11.0958 15.4792 11 15.7167 11 16C11 16.2833 11.0958 16.5208 11.2875 16.7125C11.4792 16.9042 11.7167 17 12 17ZM12 13C12.2833 13 12.5208 12.9042 12.7125 12.7125C12.9042 12.5208 13 12.2833 13 12V8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8V12C11 12.2833 11.0958 12.5208 11.2875 12.7125C11.4792 12.9042 11.7167 13 12 13ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#FB006D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.72503 21C2.5417 21 2.37503 20.9542 2.22503 20.8625C2.07503 20.7708 1.95837 20.65 1.87503 20.5C1.7917 20.35 1.74587 20.1875 1.73753 20.0125C1.7292 19.8375 1.77503 19.6667 1.87503 19.5L11.125 3.5C11.225 3.33333 11.3542 3.20833 11.5125 3.125C11.6709 3.04167 11.8334 3 12 3C12.1667 3 12.3292 3.04167 12.4875 3.125C12.6459 3.20833 12.775 3.33333 12.875 3.5L22.125 19.5C22.225 19.6667 22.2709 19.8375 22.2625 20.0125C22.2542 20.1875 22.2084 20.35 22.125 20.5C22.0417 20.65 21.925 20.7708 21.775 20.8625C21.625 20.9542 21.4584 21 21.275 21H2.72503ZM12 18C12.2834 18 12.5209 17.9042 12.7125 17.7125C12.9042 17.5208 13 17.2833 13 17C13 16.7167 12.9042 16.4792 12.7125 16.2875C12.5209 16.0958 12.2834 16 12 16C11.7167 16 11.4792 16.0958 11.2875 16.2875C11.0959 16.4792 11 16.7167 11 17C11 17.2833 11.0959 17.5208 11.2875 17.7125C11.4792 17.9042 11.7167 18 12 18ZM12 15C12.2834 15 12.5209 14.9042 12.7125 14.7125C12.9042 14.5208 13 14.2833 13 14V11C13 10.7167 12.9042 10.4792 12.7125 10.2875C12.5209 10.0958 12.2834 10 12 10C11.7167 10 11.4792 10.0958 11.2875 10.2875C11.0959 10.4792 11 10.7167 11 11V14C11 14.2833 11.0959 14.5208 11.2875 14.7125C11.4792 14.9042 11.7167 15 12 15Z" fill="#FF7E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,5 +0,0 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.3327 8.12533C15.3327 12.1754 12.0494 15.4587 7.99935 15.4587C3.94926 15.4587 0.666016 12.1754 0.666016 8.12533C0.666016 4.07524 3.94926 0.791992 7.99935 0.791992C12.0494 0.791992 15.3327 4.07524 15.3327 8.12533Z" fill="#FB006D"/>
<path d="M8.00065 4.79199C7.63246 4.79199 7.33398 5.09047 7.33398 5.45866V8.79199C7.33398 9.16018 7.63246 9.45866 8.00065 9.45866C8.36884 9.45866 8.66732 9.16018 8.66732 8.79199V5.45866C8.66732 5.09047 8.36884 4.79199 8.00065 4.79199Z" fill="white"/>
<path d="M8.00065 10.1253C7.63246 10.1253 7.33398 10.4238 7.33398 10.792C7.33398 11.1602 7.63246 11.4587 8.00065 11.4587C8.36884 11.4587 8.66732 11.1602 8.66732 10.792C8.66732 10.4238 8.36884 10.1253 8.00065 10.1253Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 830 B

Some files were not shown because too many files have changed in this diff Show more