Compare commits

...
31 Commits
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
MornandLucas.Xu 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 ShiueandLucas 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
Lucas 38c2937f64 fix: docker ci (#7087) 2024-12-30 13:35:37 +08:00
Richard Shiue 06d5bc734b fix: try to fix message regeneration being appended instead of replaced (#7084) 2024-12-30 12:38:56 +08:00
Nathan.fooo a521541cb7 chore: only sync when doucment was changed (#7081)
* chore: only sync when doucment was changed

* chore: fmt
2024-12-30 10:26:06 +08:00
Kilu.He 7d61252e6a feat: project management & document editor features on web (#7051)
* fix: support page operations

* fix: support select language of code block

* fix: support hover controls of document

* fix: support slash panel of document

* feat: support image block and file block of document

* feat: support outline block of document

* feat: support add document on document

* feat: support create sub-page on document

* feat: support mention panel

* feat: support more actions

* feat: support create space

* fix: add some shortcuts to document

* fix: add some shortcuts to document

* fix: multiple select blocks

* fix: reupdate trash after restore and delete

* fix: lint check

* fix: callout block

* fix: view icon

* fix: colors

* fix: colors

* feat: support add grid/board/calendar

* feat: support database new layout

* fix: slash panel

* fix: bugs

* fix: grid table

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: bugs

* fix: some bugs

* fix: some bugs

* fix: invite members

* fix: styles

* fix: styles

* fix: styles

* fix: inline code

* fix: image block

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: add applink

* fix: add applink

* fix: add applink

* fix: add share link

* fix: inline link

* fix: add log

* fix: simple table bugs

* fix: simple table bugs

* fix: some bugs

* fix: build size

* fix: build size

* fix: remove private from package.json

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: some bugs

* fix: support paste md

* fix: modified toolbar

* fix: modified toolbard actions

* fix: adjust editor toolbar

* fix: support billing
2024-12-30 10:12:54 +08:00
Lucas cb2b9ac537 fix: simple table issues on mobile (#7068)
* feat: use fixed height for table menu

* feat: do not close menu after enabling header column/row

* fix: remove cell background color

* fix: the text color doesn't apply to the heading block

* fix: replace quick action icon color

* fix: add done button in text color menu

* fix: don't access clipboard when opening table action menu

* fix: table navigation issues and reminder issue

* fix: cursor flash when create a row and column at the same time

* fix: unable to insert page sometimes

* fix: use default name if the title is empty

* chore: replace align icons
2024-12-30 09:23:03 +08:00
Alexandre Cirilo af31f8cf6f chore(i18n): update fr-FR translations 🐦 (#7076) 2024-12-30 09:16:52 +08:00
Richard Shiue 87c1245a3f fix: launch review issues 0.7.9 (#7073)
* fix: disable select sources when on local AI

* chore: tooltips for right sidebar actions

* chore: update selection after inserting into existing page

* fix: mobile select sources button background color

* fix: ai response metadata

* chore: don't update selected sources section until reopen

* chore: show views from all spaces in select sources menu

* chore: revert podfile changes

* chore: decrease spacing between metadata and hover actions

* chore: improve placeholder for ai message metadata
2024-12-29 21:48:37 +08:00
Nathan.fooo 49d9417cac refactor: crate dir (#7080) 2024-12-29 14:47:28 +08:00
jumbi77 1f29235538 chore(i18n): update de-DE translations 🐦 (#7071) 2024-12-28 15:39:34 +08:00
Richard Shiue fd1e36b21a fix: numbered list misalignment (#7066)
* fix: numbered list misalignment

* chore: fixed width numbers
2024-12-27 16:08:27 +08:00
553 changed files with 26769 additions and 6068 deletions

No files matched your search

+35 -3
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
+1 -1
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"
@@ -53,7 +53,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.appflowy.appflowy"
minSdkVersion 29
targetSdkVersion 34
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
@@ -2,6 +2,7 @@ import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'document/document_test_runner.dart' as document_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'sidebar/sidebar_rename_untitled_test.dart'
as sidebar_rename_untitled_test;
@@ -26,4 +27,5 @@ Future<void> main() async {
// sidebar
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
}
@@ -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);
});
}
@@ -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);
},
);
});
@@ -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 {
@@ -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 {
@@ -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);
});
});
}
@@ -67,6 +67,7 @@ void main() {
// open settings page to check the result
await tester.tapButton(settingsButton);
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// check the server type
expect(
@@ -1,9 +1,7 @@
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
@@ -27,21 +25,6 @@ void main() {
RecentIcons.enable = true;
});
Future<EmojiIconData> loadIcon() async {
await loadIconGroups();
final groups = kIconGroups!;
final firstGroup = groups.first;
final firstIcon = firstGroup.icons.first;
return EmojiIconData.icon(
IconsData(
firstGroup.name,
firstIcon.content,
firstIcon.name,
builtInSpaceColors.first,
),
);
}
testWidgets('Update page emoji in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -160,7 +143,7 @@ void main() {
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await loadIcon();
final iconData = await tester.loadIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
@@ -192,7 +175,7 @@ void main() {
testWidgets('Update page icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await loadIcon();
final iconData = await tester.loadIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -12,7 +13,14 @@ import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('Sidebar view item tests', () {
testWidgets('Access view item context menu by right click', (tester) async {
@@ -1,17 +1,30 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('document page style:', () {
double getCurrentEditorFontSize() {
@@ -114,5 +127,37 @@ void main() {
);
expect(builtInCover, findsOneWidget);
});
testWidgets('page style icon', (tester) async {
await tester.launchInAnonymousMode();
final createPageButton =
find.byKey(BottomNavigationBarItemType.add.valueKey);
await tester.tapButton(createPageButton);
/// toggle the preset button
await tester.tapSvgButton(FlowySvgs.m_layout_s);
/// select document plugins emoji
final pageStyleIcon = find.byType(PageStyleIcon);
/// there should be none of emoji
final noneText = find.text(LocaleKeys.pageStyle_none.tr());
expect(noneText, findsOneWidget);
await tester.tapButton(pageStyleIcon);
/// select an emoji
const emoji = '😄';
await tester.tapEmoji(emoji);
await tester.tapSvgButton(FlowySvgs.m_layout_s);
expect(noneText, findsNothing);
expect(
find.descendant(
of: pageStyleIcon,
matching: find.text(emoji),
),
findsOneWidget,
);
});
});
}
@@ -2,8 +2,10 @@ import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -243,6 +245,53 @@ void main() {
expect(table.isHeaderColumnEnabled, isTrue);
expect(table.isHeaderRowEnabled, isTrue);
// disable header column
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickColumnMenuButton(0);
final toggleButton = find.descendant(
of: find.byType(SimpleTableHeaderActionButton),
matching: find.byType(CupertinoSwitch),
);
await tester.tapButton(toggleButton);
}
// enable header row
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickRowMenuButton(0);
// enable header column
final toggleButton = find.descendant(
of: find.byType(SimpleTableHeaderActionButton),
matching: find.byType(CupertinoSwitch),
);
await tester.tapButton(toggleButton);
}
// check the table is updated
expect(table.isHeaderColumnEnabled, isFalse);
expect(table.isHeaderRowEnabled, isFalse);
// set to page width
{
final table = editorState.getNodeAtPath([0])!;
@@ -371,13 +420,22 @@ void main() {
// click the column menu button
await tester.clickColumnMenuButton(0);
// clear content
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_clearContents
.tr(),
),
final clearContents = find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_clearContents
.tr(),
);
// clear content
final scrollable = find.descendant(
of: find.byType(SimpleTableBottomSheet),
matching: find.byType(Scrollable),
);
await tester.scrollUntilVisible(
clearContents,
100,
scrollable: scrollable,
);
await tester.tapButton(clearContents);
await tester.cancelTableActionMenu();
// check the first cell is empty
@@ -427,7 +485,7 @@ void main() {
// open the plus menu and select the heading block
{
await tester.openPlusMenuAndClickButton(
LocaleKeys.editor_toggleHeading1ShortForm.tr(),
LocaleKeys.editor_heading1.tr(),
);
// check the heading block is inserted
@@ -175,6 +175,33 @@ extension AppFlowyTestBase on WidgetTester {
}
}
Future<void> tapDown(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
PointerDeviceKind kind = PointerDeviceKind.touch,
bool pumpAndSettle = true,
int milliseconds = 500,
}) async {
final location = getCenter(finder);
final TestGesture gesture = await startGesture(
location,
pointer: pointer,
buttons: buttons,
kind: kind,
);
await gesture.cancel();
await gesture.down(location);
await gesture.cancel();
if (pumpAndSettle) {
await this.pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 15),
);
}
}
Future<void> tapButtonWithName(
String tr, {
int milliseconds = 500,
@@ -13,6 +13,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
@@ -23,6 +24,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
@@ -898,6 +900,22 @@ extension CommonOperations on WidgetTester {
await tapAt(Offset.zero);
await pumpUntilNotFound(finder);
}
/// load icon list and return the first one
Future<EmojiIconData> loadIcon() async {
await loadIconGroups();
final groups = kIconGroups!;
final firstGroup = groups.first;
final firstIcon = firstGroup.icons.first;
return EmojiIconData.icon(
IconsData(
firstGroup.name,
firstIcon.content,
firstIcon.name,
builtInSpaceColors.first,
),
);
}
}
extension SettingsFinder on CommonFinders {
@@ -31,10 +31,7 @@ extension EmojiTestExtension on WidgetTester {
matching: find.text(PickerTabType.icon.tr),
);
expect(iconTab, findsOneWidget);
expect(find.byType(FlowyIconPicker), findsNothing);
await tap(iconTab);
await pumpAndSettle();
expect(find.byType(FlowyIconPicker), findsOneWidget);
await tapButton(iconTab);
final selectedSvg = find.descendant(
of: find.byType(FlowyIconPicker),
matching: find.byWidgetPredicate(
@@ -42,6 +39,11 @@ extension EmojiTestExtension on WidgetTester {
),
);
expect(find.byType(IconColorPicker), findsNothing);
/// test for tapping down, it should not display the ColorPicker unless tapping up
await tapDown(selectedSvg);
expect(find.byType(IconColorPicker), findsNothing);
await tapButton(selectedSvg);
final colorPicker = find.byType(IconColorPicker);
expect(colorPicker, findsOneWidget);
+3 -3
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
@@ -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,
),
@@ -57,6 +57,7 @@ Future<T?> showMobileBottomSheet<T>(
double maxChildSize = 0.8,
double initialChildSize = 0.51,
double bottomSheetPadding = 0,
bool enablePadding = true,
}) async {
assert(
showHeader ||
@@ -172,14 +173,18 @@ Future<T?> showMobileBottomSheet<T>(
}
// ----- content area -----
// add content padding and extra bottom padding
children.add(
Padding(
padding:
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
child: child,
),
);
if (enablePadding) {
// add content padding and extra bottom padding
children.add(
Padding(
padding:
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
child: child,
),
);
} else {
children.add(child);
}
// ----- content area -----
if (children.length == 1) {
@@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/built_in_svgs.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
@@ -234,6 +235,7 @@ class _UserIcon extends StatelessWidget {
queryParameters: {
MobileEmojiPickerScreen.pageTitle:
LocaleKeys.titleBar_userIcon.tr(),
MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name],
},
).toString(),
);
@@ -178,16 +178,18 @@ class MobileViewPage extends StatelessWidget {
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
WidgetSpan(
child: SizedBox(
width: 20,
child: EmojiIconWidget(
emoji: icon,
emojiSize: 17.0,
if (icon.isNotEmpty) ...[
WidgetSpan(
child: SizedBox(
width: 20,
child: EmojiIconWidget(
emoji: icon,
emojiSize: 18.0,
),
),
),
),
if (icon.isNotEmpty) const WidgetSpan(child: HSpace(2.0)),
const WidgetSpan(child: HSpace(8.0)),
],
TextSpan(
text: name,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
@@ -37,8 +37,13 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
// Only user enable chat with file and the plugin is already running
final supportChatWithFile = chatState.fileEnabled &&
chatState.pluginState.state == RunningStatePB.Running;
final aiType = chatState.pluginState.state == RunningStatePB.Running
? AIType.localAI
: AIType.appflowyAI;
emit(
state.copyWith(
aiType: aiType,
supportChatWithFile: supportChatWithFile,
chatState: chatState,
),
@@ -272,15 +272,19 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
// 3 mean message response from AI
if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) {
temporaryMessageIDMap[pb.messageId.toString()] =
answerStreamMessageId;
temporaryMessageIDMap.putIfAbsent(
pb.messageId.toString(),
() => answerStreamMessageId,
);
answerStreamMessageId = '';
}
// 1 mean message response from User
if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) {
temporaryMessageIDMap[pb.messageId.toString()] =
questionStreamMessageId;
temporaryMessageIDMap.putIfAbsent(
pb.messageId.toString(),
() => questionStreamMessageId,
);
questionStreamMessageId = '';
}
@@ -422,8 +426,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
(question) {
if (!isClosed) {
final streamAnswer = _createAnswerStreamMessage(
answerStream!,
question.messageId,
stream: answerStream!,
questionMessageId: question.messageId,
fakeQuestionMessageId: questionStreamMessage.id,
);
lastSentMessage = question;
@@ -479,8 +484,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
(success) {
if (!isClosed) {
final streamAnswer = _createAnswerStreamMessage(
answerStream!,
answerMessageId - 1,
stream: answerStream!,
questionMessageId: answerMessageId - 1,
).copyWith(id: answerMessageIdString);
add(ChatEvent.receiveMessage(streamAnswer));
@@ -491,11 +496,14 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
);
}
Message _createAnswerStreamMessage(
AnswerStream stream,
Int64 questionMessageId,
) {
answerStreamMessageId = (questionMessageId + 1).toString();
Message _createAnswerStreamMessage({
required AnswerStream stream,
required Int64 questionMessageId,
String? fakeQuestionMessageId,
}) {
answerStreamMessageId = fakeQuestionMessageId == null
? (questionMessageId + 1).toString()
: "${fakeQuestionMessageId}_ans";
return TextMessage(
id: answerStreamMessageId,
@@ -84,8 +84,10 @@ class ChatEditDocumentService {
final nodes = [
if (rootIsEmpty || !isLastLineEmpty) paragraphNode(),
...messageDocument.root.children,
paragraphNode(),
];
final insertPath = rootIsEmpty || listEquals(lastNodeOrNull.path, const [0])
final insertPath = rootIsEmpty ||
listEquals(lastNodeOrNull.path, const [0]) && isLastLineEmpty
? const [0]
: lastNodeOrNull.path.next;
@@ -4,6 +4,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -103,7 +104,7 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
ChatSettingsCubit() : super(ChatSettingsState.initial());
List<String> selectedSourceIds = [];
ChatSource? source;
List<ChatSource> sources = [];
List<ChatSource> selectedSources = [];
String filter = '';
@@ -111,25 +112,32 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
selectedSourceIds = [...newSelectedSourceIds];
}
void refreshSources(ViewPB view) async {
void refreshSources(List<ViewPB> spaceViews) async {
filter = "";
final newSource = await _recursiveBuild(view, null);
_restrictSelectionIfNecessary(newSource.children);
final newSources = await Future.wait(
spaceViews.map((view) => _recursiveBuild(view, null)),
);
for (final source in newSources) {
_restrictSelectionIfNecessary(source.children);
}
final selected = _buildSelectedSources(newSource).toList();
newSource.toggleIsExpanded();
final selected = newSources
.map((source) => _buildSelectedSources(source))
.flattened
.toList();
emit(
state.copyWith(
selectedSources: selected,
visibleSources: [newSource],
visibleSources: newSources,
),
);
source?.dispose();
source = newSource.copy();
sources
..forEach((e) => e.dispose())
..clear()
..addAll(newSources.map((e) => e.copy()));
selectedSources
..forEach((e) => e.dispose())
@@ -204,12 +212,13 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
for (final source in state.visibleSources) {
source.dispose();
}
if (source == null) {
if (sources.isEmpty) {
emit(ChatSettingsState.initial());
} else {
final selected =
selectedSources.map(_buildSearchResults).nonNulls.toList();
final visible = [_buildSearchResults(source!)].nonNulls.toList();
final visible =
sources.map(_buildSearchResults).nonNulls.nonNulls.toList();
emit(
state.copyWith(
selectedSources: selected,
@@ -301,25 +310,21 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
}
void updateSelectedStatus() {
if (source == null) {
if (sources.isEmpty) {
return;
}
_recursiveUpdateSelectedStatus(source!);
_restrictSelectionIfNecessary(source!.children);
for (final source in sources) {
_recursiveUpdateSelectedStatus(source);
}
_restrictSelectionIfNecessary(sources);
for (final visibleSource in state.visibleSources) {
visibleSource.dispose();
}
final visible = [_buildSearchResults(source!)].nonNulls.toList();
final visible = sources.map(_buildSearchResults).nonNulls.toList();
selectedSources
..forEach((e) => e.dispose())
..clear()
..addAll(_buildSelectedSources(source!));
emit(
state.copyWith(
visibleSources: visible,
selectedSources:
selectedSources.map(_buildSearchResults).nonNulls.toList(),
),
);
}
@@ -360,13 +365,24 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
?.toggleIsExpanded();
}
} else {
source?.findChildBySourceId(chatSource.view.id)?.toggleIsExpanded();
for (final source in sources) {
final child = source.findChildBySourceId(chatSource.view.id);
if (child != null) {
child.toggleIsExpanded();
break;
}
}
}
}
@override
Future<void> close() {
source?.dispose();
for (final child in sources) {
child.dispose();
}
for (final child in selectedSources) {
child.dispose();
}
for (final child in state.selectedSources) {
child.dispose();
}
@@ -49,7 +49,7 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer {
DefaultAppearanceSettings.getDefaultSelectionColor(context),
defaultTextDirection: appearance.defaultTextDirection,
textStyleConfiguration: TextStyleConfiguration(
lineHeight: 1.4,
lineHeight: 20 / 14,
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
text: baseTextStyle(fontFamily).copyWith(
@@ -506,6 +506,12 @@ class _PromptBottomActions extends StatelessWidget {
margin: DesktopAIPromptSizes.actionBarPadding,
child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(
builder: (context, state) {
if (state.chatState == null) {
return Align(
alignment: AlignmentDirectional.centerEnd,
child: _sendButton(),
);
}
return Row(
children: [
// predefinedFormatButton(),
@@ -312,7 +312,7 @@ class _LeadingActions extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).cardColor,
color: Theme.of(context).colorScheme.surface,
child: PromptInputMobileSelectSourcesButton(
onUpdateSelectedSources: onUpdateSelectedSources,
),
@@ -69,9 +69,9 @@ class _PromptInputMobileSelectSourcesButtonState
value: cubit,
),
],
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
selector: (state) => state.currentSpace,
builder: (context, spaceView) {
child: BlocSelector<SpaceBloc, SpaceState, List<ViewPB>>(
selector: (state) => state.spaces,
builder: (context, spaces) {
return BlocListener<ChatBloc, ChatState>(
listener: (context, state) {
cubit
@@ -97,11 +97,7 @@ class _PromptInputMobileSelectSourcesButtonState
],
),
onTap: () async {
if (spaceView != null) {
context
.read<ChatSettingsCubit>()
.refreshSources(spaceView);
}
context.read<ChatSettingsCubit>().refreshSources(spaces);
await showMobileBottomSheet<void>(
context,
backgroundColor: Theme.of(context).colorScheme.surface,
@@ -74,9 +74,9 @@ class _PromptInputDesktopSelectSourcesButtonState
value: cubit,
),
],
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
selector: (state) => state.currentSpace,
builder: (context, spaceView) {
child: BlocSelector<SpaceBloc, SpaceState, List<ViewPB>>(
selector: (state) => state.spaces,
builder: (context, spaces) {
return BlocListener<ChatBloc, ChatState>(
listener: (context, state) {
cubit
@@ -90,15 +90,11 @@ class _PromptInputDesktopSelectSourcesButtonState
margin: EdgeInsets.zero,
controller: popoverController,
onOpen: () {
if (spaceView != null) {
context.read<ChatSettingsCubit>().refreshSources(spaceView);
}
context.read<ChatSettingsCubit>().refreshSources(spaces);
},
onClose: () {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
if (spaceView != null) {
context.read<ChatSettingsCubit>().refreshSources(spaceView);
}
context.read<ChatSettingsCubit>().refreshSources(spaces);
},
popupBuilder: (_) {
return BlocProvider.value(
@@ -395,12 +391,19 @@ class ChatSourceTreeItemInner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlowyHover(
cursor: isSelectedSection ? SystemMouseCursors.basic : null,
style: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: isSelectedSection
? Colors.transparent
: AFThemeExtension.of(context).lightGreyHover,
),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => onSelected?.call(chatSource),
onTap: () {
if (!isSelectedSection) {
onSelected?.call(chatSource);
}
},
child: Row(
children: [
const HSpace(4.0),
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -19,6 +20,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -243,7 +245,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
constraints: const BoxConstraints.tightFor(width: 300, height: 400),
onClose: () {
if (spaceView != null) {
context.read<ChatSettingsCubit>().refreshSources(spaceView);
context.read<ChatSettingsCubit>().refreshSources([spaceView]);
}
widget.onOverrideVisibility?.call(false);
},
@@ -272,12 +274,12 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
onPressed: () async {
final documentId = getOpenedDocumentId();
if (documentId != null) {
await onAddToExistingPage(documentId);
DocumentBloc.findOpen(documentId)?.forceReloadDocumentState();
await onAddToExistingPage(context, documentId);
await forceReloadAndUpdateSelection(documentId);
} else {
widget.onOverrideVisibility?.call(true);
if (spaceView != null) {
context.read<ChatSettingsCubit>().refreshSources(spaceView);
context.read<ChatSettingsCubit>().refreshSources([spaceView]);
}
popoverController.show();
}
@@ -296,9 +298,8 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
},
onAddToExistingPage: (documentId) async {
popoverController.close();
await onAddToExistingPage(documentId);
final view =
await ViewBackendService.getView(documentId).toNullable();
final view = await onAddToExistingPage(context, documentId);
if (context.mounted) {
openPageFromMessage(context, view);
}
@@ -307,12 +308,20 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
);
}
Future<void> onAddToExistingPage(String documentId) async {
Future<ViewPB?> onAddToExistingPage(
BuildContext context,
String documentId,
) async {
await ChatEditDocumentService.addMessageToPage(
documentId,
widget.textMessage,
);
await Future.delayed(const Duration(milliseconds: 500));
final view = await ViewBackendService.getView(documentId).toNullable();
if (context.mounted) {
showSaveMessageSuccessToast(context, view);
}
return view;
}
void addMessageToNewPage(BuildContext context) async {
@@ -325,12 +334,63 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
chatView.parentViewId,
[widget.textMessage],
);
if (context.mounted) {
showSaveMessageSuccessToast(context, newView);
openPageFromMessage(context, newView);
}
}
}
void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
if (view == null) {
return;
}
showToastNotification(
context,
richMessage: TextSpan(
children: [
TextSpan(
text: LocaleKeys.chat_addToNewPageSuccessToast.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: const Color(0xFFFFFFFF),
),
),
const TextSpan(
text: ' ',
),
TextSpan(
text: view.nameOrDefault,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: const Color(0xFFFFFFFF),
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Future<void> forceReloadAndUpdateSelection(String documentId) async {
final bloc = DocumentBloc.findOpen(documentId);
if (bloc == null) {
return;
}
await bloc.forceReloadDocumentState();
await Future.delayed(const Duration(milliseconds: 500));
final editorState = bloc.state.editorState;
final lastNodePath = editorState?.getLastSelectable()?.$1.path;
if (editorState == null || lastNodePath == null) {
return;
}
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: lastNodePath)),
),
);
}
String? getOpenedDocumentId() {
final pageManager = getIt<TabsBloc>().state.currentPageManager;
if (!pageManager.showSecondaryPluginNotifier.value) {
@@ -1,11 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
import 'package:time/time.dart';
class AIMessageMetadata extends StatefulWidget {
@@ -68,30 +74,44 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
runSpacing: 4.0,
children: widget.sources.map(
(m) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 24,
maxWidth: 240,
),
child: FlowyButton(
margin: const EdgeInsets.all(4.0),
useIntrinsicWidth: true,
radius: BorderRadius.circular(8.0),
text: FlowyText(
m.name,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
leftIcon: FlowySvg(
FlowySvgs.icon_document_s,
size: const Size.square(16),
color: Theme.of(context).hintColor,
),
onTap: () {
widget.onSelectedMetadata?.call(m);
if (isURL(m.id)) {
return _MetadataButton(
name: m.id,
onTap: () => widget.onSelectedMetadata?.call(m),
);
} else if (isUUID(m.id)) {
return FutureBuilder<ViewPB?>(
future: ViewBackendService.getView(m.id)
.then((f) => f.toNullable()),
builder: (context, snapshot) {
final data = snapshot.data;
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done ||
data == null) {
return _MetadataButton(
name: m.name,
onTap: () => widget.onSelectedMetadata?.call(m),
);
}
return BlocProvider(
create: (_) => ViewBloc(view: data),
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return _MetadataButton(
name: state.view.nameOrDefault,
onTap: () => widget.onSelectedMetadata?.call(m),
);
},
),
);
},
),
);
);
} else {
return _MetadataButton(
name: m.name,
onTap: () => widget.onSelectedMetadata?.call(m),
);
}
},
).toList(),
),
@@ -101,3 +121,39 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
);
}
}
class _MetadataButton extends StatelessWidget {
const _MetadataButton({
this.name = "",
this.onTap,
});
final String name;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 24,
maxWidth: 240,
),
child: FlowyButton(
margin: const EdgeInsets.all(4.0),
useIntrinsicWidth: true,
radius: BorderRadius.circular(8.0),
text: FlowyText(
name,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
leftIcon: FlowySvg(
FlowySvgs.icon_document_s,
size: const Size.square(16),
color: Theme.of(context).hintColor,
),
onTap: onTap,
),
);
}
}
@@ -100,7 +100,7 @@ class ChatAIMessageWidget extends StatelessWidget {
onSelectedMetadata: onSelectedMetadata,
),
if (state.sources.isNotEmpty && !isLastMessage)
const VSpace(16.0),
const VSpace(8.0),
],
),
);
@@ -58,7 +58,7 @@ class _ChatErrorMessageWidgetState extends State<ChatErrorMessageWidget> {
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.warning_filled_s,
FlowySvgs.toast_error_filled_s,
blendMode: null,
),
const HSpace(8.0),
@@ -1,8 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart';
import 'package:universal_platform/universal_platform.dart';
@@ -10,6 +13,11 @@ import 'package:universal_platform/universal_platform.dart';
/// on mobile
void openPageFromMessage(BuildContext context, ViewPB? view) {
if (view == null) {
showToastNotification(
context,
message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),
type: ToastificationType.error,
);
return;
}
if (UniversalPlatform.isDesktop) {
@@ -13,6 +13,18 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
EmojiData? kCachedEmojiData;
const _kRecentEmojiCategoryId = 'Recent';
class EmojiPickerResult {
EmojiPickerResult({
required this.emojiId,
required this.emoji,
this.isRandom = false,
});
final String emojiId;
final String emoji;
final bool isRandom;
}
class FlowyEmojiPicker extends StatefulWidget {
const FlowyEmojiPicker({
super.key,
@@ -21,7 +33,7 @@ class FlowyEmojiPicker extends StatefulWidget {
this.ensureFocus = false,
});
final EmojiSelectedCallback onEmojiSelected;
final ValueChanged<EmojiPickerResult> onEmojiSelected;
final int emojiPerLine;
final bool ensureFocus;
@@ -70,7 +82,9 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
),
onEmojiSelected: (id, emoji) {
widget.onEmojiSelected.call(id, emoji);
widget.onEmojiSelected.call(
EmojiPickerResult(emojiId: id, emoji: emoji),
);
RecentIcons.putEmoji(id);
},
padding: const EdgeInsets.symmetric(horizontal: 16.0),
@@ -106,7 +120,12 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
onSkinToneChanged: (value) {
skinTone.value = value;
},
onRandomEmojiSelected: widget.onEmojiSelected,
onRandomEmojiSelected: (id, emoji) {
widget.onEmojiSelected.call(
EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true),
);
RecentIcons.putEmoji(id);
},
),
);
},
@@ -11,14 +11,17 @@ class MobileEmojiPickerScreen extends StatelessWidget {
const MobileEmojiPickerScreen({
super.key,
this.title,
this.selectedType,
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
});
final PickerTabType? selectedType;
final String? title;
final List<PickerTabType> tabs;
static const routeName = '/emoji_picker';
static const pageTitle = 'title';
static const iconSelectedType = 'iconSelectedType';
static const selectTabs = 'tabs';
@override
@@ -30,8 +33,9 @@ class MobileEmojiPickerScreen extends StatelessWidget {
body: SafeArea(
child: FlowyIconEmojiPicker(
tabs: tabs,
initialType: selectedType,
onSelectedEmoji: (r) {
context.pop<EmojiIconData>(r);
context.pop<EmojiIconData>(r.data);
},
),
),
@@ -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,
@@ -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,
@@ -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,
);
}
}
@@ -244,9 +244,9 @@ class _RenameRowPopoverState extends State<RenameRowPopover> {
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 18),
defaultIcon: const FlowySvg(FlowySvgs.document_s),
onSubmitted: (emoji, _) {
widget.onUpdateIcon(emoji);
PopoverContainer.of(context).close();
onSubmitted: (r, _) {
widget.onUpdateIcon(r.data);
if (!r.keepOpen) PopoverContainer.of(context).close();
},
),
const HSpace(6),
@@ -422,8 +422,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
);
}
void forceReloadDocumentState() {
_documentCollabAdapter.syncV3();
Future<void> forceReloadDocumentState() {
return _documentCollabAdapter.syncV3();
}
// this is only used for debug mode
@@ -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;
@@ -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 {
@@ -399,10 +399,11 @@ ParagraphBlockComponentBuilder _buildParagraphBlockComponentBuilder(
return ParagraphBlockComponentBuilder(
configuration: configuration.copyWith(
placeholderText: placeholderText,
textStyle: (node) => _buildTextStyleInTableCell(
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
),
textAlign: (node) => _buildTextAlignInTableCell(
context,
@@ -421,10 +422,11 @@ TodoListBlockComponentBuilder _buildTodoListBlockComponentBuilder(
return TodoListBlockComponentBuilder(
configuration: configuration.copyWith(
placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(),
textStyle: (node) => _buildTextStyleInTableCell(
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
),
textAlign: (node) => _buildTextAlignInTableCell(
context,
@@ -451,10 +453,11 @@ BulletedListBlockComponentBuilder _buildBulletedListBlockComponentBuilder(
return BulletedListBlockComponentBuilder(
configuration: configuration.copyWith(
placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(),
textStyle: (node) => _buildTextStyleInTableCell(
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
),
textAlign: (node) => _buildTextAlignInTableCell(
context,
@@ -473,10 +476,11 @@ NumberedListBlockComponentBuilder _buildNumberedListBlockComponentBuilder(
return NumberedListBlockComponentBuilder(
configuration: configuration.copyWith(
placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(),
textStyle: (node) => _buildTextStyleInTableCell(
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
),
textAlign: (node) => _buildTextAlignInTableCell(
context,
@@ -507,10 +511,11 @@ QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder(
return QuoteBlockComponentBuilder(
configuration: configuration.copyWith(
placeholderText: (_) => LocaleKeys.blockPlaceholders_quote.tr(),
textStyle: (node) => _buildTextStyleInTableCell(
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
),
textAlign: (node) => _buildTextAlignInTableCell(
context,
@@ -529,6 +534,12 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
) {
return HeadingBlockComponentBuilder(
configuration: configuration.copyWith(
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
),
padding: (node) {
if (customHeadingPadding != null) {
return customHeadingPadding;
@@ -567,7 +578,9 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
configuration: configuration,
),
),
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
textStyleBuilder: (level) {
return styleCustomizer.headingStyleBuilder(level);
},
);
}
@@ -663,7 +676,12 @@ DatabaseViewBlockComponentBuilder _buildDatabaseViewBlockComponentBuilder(
) {
return DatabaseViewBlockComponentBuilder(
configuration: configuration.copyWith(
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
padding: (node) {
if (UniversalPlatform.isMobile) {
return configuration.padding(node);
}
return const EdgeInsets.symmetric(vertical: 10);
},
),
);
}
@@ -686,10 +704,11 @@ CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder(
node: node,
configuration: configuration,
),
textStyle: (node) => _buildTextStyleInTableCell(
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
),
),
inlinePadding: const EdgeInsets.symmetric(vertical: 8.0),
@@ -777,11 +796,12 @@ ToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder(
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
},
textStyle: (node) {
textStyle: (node, {TextSpan? textSpan}) {
final textStyle = _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
textSpan: textSpan,
);
final level = node.attributes[ToggleListBlockKeys.level] as int?;
if (level == null) {
@@ -816,9 +836,14 @@ OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder(
) {
return OutlineBlockComponentBuilder(
configuration: configuration.copyWith(
placeholderTextStyle: (_) =>
placeholderTextStyle: (node, {TextSpan? textSpan}) =>
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
padding: (node) {
if (UniversalPlatform.isMobile) {
return configuration.padding(node);
}
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
},
),
);
}
@@ -829,7 +854,12 @@ LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(
) {
return LinkPreviewBlockComponentBuilder(
configuration: configuration.copyWith(
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
padding: (node) {
if (UniversalPlatform.isMobile) {
return configuration.padding(node);
}
return const EdgeInsets.symmetric(vertical: 10);
},
),
cache: LinkPreviewDataCache(),
showMenu: true,
@@ -865,7 +895,14 @@ SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder(
}) {
return SubPageBlockComponentBuilder(
configuration: configuration.copyWith(
textStyle: (node) => styleCustomizer.subPageBlockTextStyleBuilder(),
textStyle: (node, {TextSpan? textSpan}) =>
styleCustomizer.subPageBlockTextStyleBuilder(),
padding: (node) {
if (UniversalPlatform.isMobile) {
return const EdgeInsets.symmetric(horizontal: 18);
}
return configuration.padding(node);
},
),
);
}
@@ -874,8 +911,9 @@ TextStyle _buildTextStyleInTableCell(
BuildContext context, {
required Node node,
required BlockComponentConfiguration configuration,
required TextSpan? textSpan,
}) {
TextStyle textStyle = configuration.textStyle(node);
TextStyle textStyle = configuration.textStyle(node, textSpan: textSpan);
if (node.isInHeaderColumn ||
node.isInHeaderRow ||
@@ -888,6 +926,11 @@ TextStyle _buildTextStyleInTableCell(
final cellTextColor = node.textColorInColumn ?? node.textColorInRow;
// enable it if we need to support the text color of the text span
// final isTextSpanColorNull = textSpan?.style?.color == null;
// final isTextSpanChildrenColorNull =
// textSpan?.children?.every((e) => e.style?.color == null) ?? true;
if (cellTextColor != null) {
textStyle = textStyle.copyWith(
color: buildEditorCustomizedColor(
@@ -1,14 +1,14 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/patterns/file_type_patterns.dart';
import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
const _excludeFromDropTarget = [
@@ -16,6 +16,9 @@ const _excludeFromDropTarget = [
CustomImageBlockKeys.type,
MultiImageBlockKeys.type,
FileBlockKeys.type,
SimpleTableBlockKeys.type,
SimpleTableCellBlockKeys.type,
SimpleTableRowBlockKeys.type,
];
class EditorDropHandler extends StatelessWidget {
@@ -38,8 +41,13 @@ class EditorDropHandler extends StatelessWidget {
Widget build(BuildContext context) {
final childWidget = Consumer<EditorDropManagerState>(
builder: (context, dropState, _) => DragTarget<ViewPB>(
onLeave: (_) => editorState.selectionService.removeDropTarget(),
onLeave: (_) {
editorState.selectionService.removeDropTarget();
disableAutoScrollWhenDragging = false;
},
onMove: (details) {
disableAutoScrollWhenDragging = true;
if (details.data.id == viewId) {
return;
}
@@ -505,6 +505,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
Position(path: lastNode.path),
);
}
transaction.customSelectionType = SelectionType.inline;
transaction.reason = SelectionUpdateReason.uiEvent;
await editorState.apply(transaction);
}
@@ -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;
}
@@ -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;
@@ -27,11 +27,11 @@ enum OptionAlignType {
FlowySvgData get svg {
switch (this) {
case OptionAlignType.left:
return FlowySvgs.align_left_s;
return FlowySvgs.table_align_left_s;
case OptionAlignType.center:
return FlowySvgs.align_center_s;
return FlowySvgs.table_align_center_s;
case OptionAlignType.right:
return FlowySvgs.align_right_s;
return FlowySvgs.table_align_right_s;
}
}
@@ -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,
@@ -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,
@@ -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);
},
),
),
@@ -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);
@@ -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;
@@ -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: [
@@ -173,8 +173,8 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Row(
mainAxisSize: MainAxisSize.min,
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
'@$formattedDate',
@@ -10,6 +10,7 @@ import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/shared/clipboard_state.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
@@ -510,7 +511,10 @@ class _MentionPageBlockContent extends StatelessWidget {
);
if (blockContent == null || blockContent.isEmpty) {
return shouldDisplayViewName ? view.name : '';
return shouldDisplayViewName
? view.name
.orDefault(LocaleKeys.menuAppHeader_defaultNewPageName.tr())
: '';
}
return shouldDisplayViewName
@@ -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)
@@ -19,26 +19,29 @@ class NumberedListIcon extends StatelessWidget {
Widget build(BuildContext context) {
final textStyleConfiguration =
context.read<EditorState>().editorStyle.textStyleConfiguration;
final fontSize = textStyleConfiguration.text.fontSize ?? 16.0;
final height =
textStyleConfiguration.text.height ?? textStyleConfiguration.lineHeight;
final combinedTextStyle = textStyle?.combine(textStyleConfiguration.text) ??
textStyleConfiguration.text;
final size = fontSize * height;
return Container(
constraints: BoxConstraints(
minWidth: size,
minHeight: size,
),
margin: const EdgeInsets.only(top: 0.5, right: 8.0),
alignment: Alignment.center,
child: Center(
child: Text(
node.levelString,
style: combinedTextStyle,
strutStyle: StrutStyle.fromTextStyle(combinedTextStyle),
textDirection: textDirection,
final adjustedTextStyle = combinedTextStyle.copyWith(
height: height,
fontFeatures: [const FontFeature.tabularFigures()],
);
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
node.levelString,
style: adjustedTextStyle,
strutStyle: StrutStyle.fromTextStyle(combinedTextStyle),
textHeightBehavior: TextHeightBehavior(
applyHeightToFirstAscent:
textStyleConfiguration.applyHeightToFirstAscent,
applyHeightToLastDescent:
textStyleConfiguration.applyHeightToLastDescent,
leadingDistribution: textStyleConfiguration.leadingDistribution,
),
textDirection: textDirection,
),
);
}
@@ -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,
@@ -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);
},
),
),
@@ -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;
}
}
@@ -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(
@@ -456,7 +456,7 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
return context.simpleTableDefaultHeaderColor;
}
return Theme.of(context).colorScheme.surface;
return Colors.transparent;
}
bool _isInHeader() {
@@ -86,6 +86,9 @@ class SimpleTableContext {
/// This value is available on mobile only
final ValueNotifier<int?> isReorderingHitIndex = ValueNotifier(null);
/// Scroll controller for the table
ScrollController? horizontalScrollController;
void _onHoveringOnColumnsAndRowsChanged() {
if (!_enableTableDebugLog) {
return;
@@ -269,6 +272,8 @@ class SimpleTableConstants {
static const actionSheetContentSectionHeight = 44.0;
static const actionSheetNormalActionSectionHeight = 48.0;
static const actionSheetButtonRadius = 12.0;
static const actionSheetBottomSheetHeight = 320.0;
}
enum SimpleTableBorderRenderType {
@@ -308,4 +313,7 @@ extension SimpleTableColors on BuildContext {
Color get simpleTableInsertActionBackgroundColor => Theme.of(this).isLightMode
? const Color(0xFFF2F2F7)
: const Color(0xFF2D3036);
Color? get simpleTableQuickActionBackgroundColor =>
Theme.of(this).isLightMode ? null : const Color(0xFFBBC3CD);
}
@@ -438,6 +438,8 @@ class SimpleTableActionMenu extends StatelessWidget {
return;
}
final beforeSelection = editorState.selection;
// increase the keep editor focus notifier to prevent the editor from losing focus
keepEditorFocusNotifier.increase();
@@ -469,6 +471,8 @@ class SimpleTableActionMenu extends StatelessWidget {
context,
showDragHandle: true,
showDivider: false,
useSafeArea: false,
enablePadding: false,
builder: (context) => Provider.value(
value: simpleTableContext,
child: SimpleTableBottomSheet(
@@ -482,7 +486,13 @@ class SimpleTableActionMenu extends StatelessWidget {
keepEditorFocusNotifier.decrease();
// remove the extra info
editorState.selectionType = null;
editorState.selectionExtraInfo = null;
if (beforeSelection != null) {
await editorState.updateSelectionWithReason(
beforeSelection,
customSelectionType: SelectionType.inline,
reason: SelectionUpdateReason.uiEvent,
extraInfo: {},
);
}
}
}
@@ -137,8 +137,8 @@ extension TableContentOperation on EditorState {
final cell = columnIndex >= row.children.length
? row.children.last
: row.children[columnIndex];
final startNode = cell.getFirstChildIndex();
final endNode = cell.getLastChildIndex();
final startNode = cell.getFirstFocusableChild();
final endNode = cell.getLastFocusableChild();
if (startNode == null || endNode == null) {
continue;
}
@@ -200,8 +200,8 @@ extension TableContentOperation on EditorState {
final row = tableNode.children[rowIndex];
for (var i = 0; i < row.children.length; i++) {
final cell = row.children[i];
final startNode = cell.getFirstChildIndex();
final endNode = cell.getLastChildIndex();
final startNode = cell.getFirstFocusableChild();
final endNode = cell.getLastFocusableChild();
if (startNode == null || endNode == null) {
continue;
}
@@ -255,8 +255,8 @@ extension TableContentOperation on EditorState {
final row = tableNode.children[i];
for (var j = 0; j < row.children.length; j++) {
final cell = row.children[j];
final startNode = cell.getFirstChildIndex();
final endNode = cell.getLastChildIndex();
final startNode = cell.getFirstFocusableChild();
final endNode = cell.getLastFocusableChild();
if (startNode == null || endNode == null) {
continue;
}
@@ -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');
@@ -805,21 +805,21 @@ extension TableNodeExtension on Node {
/// Get first focusable child in the table cell.
///
/// If the current node is not a table cell node, it will return null.
Node? getFirstChildIndex() {
Node? getFirstFocusableChild() {
if (children.isEmpty) {
return this;
}
return children.first.getFirstChildIndex();
return children.first.getFirstFocusableChild();
}
/// Get last focusable child in the table cell.
///
/// If the current node is not a table cell node, it will return null.
Node? getLastChildIndex() {
Node? getLastFocusableChild() {
if (children.isEmpty) {
return this;
}
return children.last.getLastChildIndex();
return children.last.getLastFocusableChild();
}
/// Get table align of column
@@ -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);
}
}
}
@@ -1,5 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@@ -40,6 +42,7 @@ KeyEventResult _arrowDownInTableCellHandler(EditorState editorState) {
if (rowIndex == parentTableNode.rowLength - 1) {
// focus on the next block
final nextNode = tableCellNode.next;
final nextBlock = tableCellNode.parentTableNode?.next;
if (nextNode != null) {
final nextFocusableSibling = parentTableNode.getNextFocusableSibling();
if (nextFocusableSibling != null) {
@@ -51,6 +54,16 @@ KeyEventResult _arrowDownInTableCellHandler(EditorState editorState) {
),
);
}
} else if (nextBlock != null) {
if (nextBlock.type != SimpleTableBlockKeys.type) {
newSelection = Selection.collapsed(
Position(
path: nextBlock.path,
),
);
} else {
return tableNavigationArrowDownCommand.handler(editorState);
}
}
} else {
// focus on next cell in the same column
@@ -4,10 +4,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart';
final simpleTableCommands = [
tableNavigationArrowDownCommand,
arrowUpInTableCell,
arrowDownInTableCell,
arrowLeftInTableCell,
@@ -0,0 +1,40 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final CommandShortcutEvent tableNavigationArrowDownCommand =
CommandShortcutEvent(
key: 'table navigation',
getDescription: () => 'table navigation',
command: 'arrow down',
handler: _tableNavigationArrowDownHandler,
);
KeyEventResult _tableNavigationArrowDownHandler(EditorState editorState) {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return KeyEventResult.ignored;
}
final nextNode = editorState.getNodeAtPath(selection.start.path.next);
if (nextNode == null) {
return KeyEventResult.ignored;
}
if (nextNode.type == SimpleTableBlockKeys.type) {
final firstCell = nextNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
if (firstCell != null) {
final firstFocusableChild = firstCell.getFirstFocusableChild();
if (firstFocusableChild != null) {
editorState.updateSelectionWithReason(
Selection.collapsed(
Position(path: firstFocusableChild.path),
),
);
return KeyEventResult.handled;
}
}
}
return KeyEventResult.ignored;
}
@@ -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();
@@ -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();
@@ -61,19 +61,12 @@ class SimpleTableCellQuickActions extends ISimpleTableBottomSheetActions {
SimpleTableMoreAction.copy,
),
),
FutureBuilder(
future: getIt<ClipboardService>().getData(),
builder: (context, snapshot) {
final hasContent = snapshot.data?.tableJson != null;
return SimpleTableQuickAction(
type: SimpleTableMoreAction.paste,
isEnabled: hasContent,
onTap: () => _onActionTap(
context,
SimpleTableMoreAction.paste,
),
);
},
SimpleTableQuickAction(
type: SimpleTableMoreAction.paste,
onTap: () => _onActionTap(
context,
SimpleTableMoreAction.paste,
),
),
SimpleTableQuickAction(
type: SimpleTableMoreAction.delete,
@@ -208,8 +201,10 @@ class SimpleTableQuickAction extends StatelessWidget {
onTapUp: isEnabled ? onTap : null,
child: FlowySvg(
type.leftIconSvg,
blendMode: null,
blendMode:
type == SimpleTableMoreAction.delete ? null : BlendMode.srcIn,
size: const Size.square(24),
color: context.simpleTableQuickActionBackgroundColor,
),
),
);
@@ -244,18 +239,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
SimpleTableInsertAction(
type: SimpleTableMoreAction.insertAbove,
enableLeftBorder: true,
onTap: () => _onActionTap(
onTap: (increaseCounter) async => _onActionTap(
context,
SimpleTableMoreAction.insertAbove,
type: SimpleTableMoreAction.insertAbove,
increaseCounter: increaseCounter,
),
),
const HSpace(2),
SimpleTableInsertAction(
type: SimpleTableMoreAction.insertBelow,
enableRightBorder: true,
onTap: () => _onActionTap(
onTap: (increaseCounter) async => _onActionTap(
context,
SimpleTableMoreAction.insertBelow,
type: SimpleTableMoreAction.insertBelow,
increaseCounter: increaseCounter,
),
),
],
@@ -265,18 +262,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
SimpleTableInsertAction(
type: SimpleTableMoreAction.insertLeft,
enableLeftBorder: true,
onTap: () => _onActionTap(
onTap: (increaseCounter) async => _onActionTap(
context,
SimpleTableMoreAction.insertLeft,
type: SimpleTableMoreAction.insertLeft,
increaseCounter: increaseCounter,
),
),
const HSpace(2),
SimpleTableInsertAction(
type: SimpleTableMoreAction.insertRight,
enableRightBorder: true,
onTap: () => _onActionTap(
onTap: (increaseCounter) async => _onActionTap(
context,
SimpleTableMoreAction.insertRight,
type: SimpleTableMoreAction.insertRight,
increaseCounter: increaseCounter,
),
),
],
@@ -284,7 +283,11 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
};
}
void _onActionTap(BuildContext context, SimpleTableMoreAction type) {
Future<void> _onActionTap(
BuildContext context, {
required SimpleTableMoreAction type,
required int increaseCounter,
}) async {
final simpleTableContext = context.read<SimpleTableContext>();
final tableNode = cellNode.parentTableNode;
if (tableNode == null) {
@@ -296,34 +299,48 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
case SimpleTableMoreAction.insertAbove:
// update the highlight status for the selecting row
simpleTableContext.selectingRow.value = cellNode.rowIndex + 1;
editorState.insertRowInTable(
await editorState.insertRowInTable(
tableNode,
cellNode.rowIndex,
);
case SimpleTableMoreAction.insertBelow:
editorState.insertRowInTable(
await editorState.insertRowInTable(
tableNode,
cellNode.rowIndex + 1,
);
// scroll to the next cell position
editorState.scrollService?.scrollTo(
SimpleTableConstants.defaultRowHeight,
duration: Durations.short3,
);
case SimpleTableMoreAction.insertLeft:
// update the highlight status for the selecting column
simpleTableContext.selectingColumn.value = cellNode.columnIndex + 1;
editorState.insertColumnInTable(
await editorState.insertColumnInTable(
tableNode,
cellNode.columnIndex,
);
case SimpleTableMoreAction.insertRight:
editorState.insertColumnInTable(
await editorState.insertColumnInTable(
tableNode,
cellNode.columnIndex + 1,
);
final horizontalScrollController =
simpleTableContext.horizontalScrollController;
if (horizontalScrollController != null) {
final previousWidth = horizontalScrollController.offset;
horizontalScrollController.jumpTo(
previousWidth + SimpleTableConstants.defaultColumnWidth,
);
}
default:
assert(false, 'Unsupported action: $type');
}
}
}
class SimpleTableInsertAction extends StatelessWidget {
class SimpleTableInsertAction extends StatefulWidget {
const SimpleTableInsertAction({
super.key,
required this.type,
@@ -335,7 +352,16 @@ class SimpleTableInsertAction extends StatelessWidget {
final SimpleTableMoreAction type;
final bool enableLeftBorder;
final bool enableRightBorder;
final void Function() onTap;
final ValueChanged<int> onTap;
@override
State<SimpleTableInsertAction> createState() =>
_SimpleTableInsertActionState();
}
class _SimpleTableInsertActionState extends State<SimpleTableInsertAction> {
// used to count how many times the action is tapped
int increaseCounter = 0;
@override
Widget build(BuildContext context) {
@@ -346,19 +372,19 @@ class SimpleTableInsertAction extends StatelessWidget {
shape: _buildBorder(),
),
child: AnimatedGestureDetector(
onTapUp: onTap,
onTapUp: () => widget.onTap(increaseCounter++),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(1),
child: FlowySvg(
type.leftIconSvg,
widget.type.leftIconSvg,
size: const Size.square(22),
),
),
FlowyText(
type.name,
widget.type.name,
fontSize: 12,
figmaLineHeight: 16,
),
@@ -375,10 +401,10 @@ class SimpleTableInsertAction extends StatelessWidget {
);
return RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: enableLeftBorder ? radius : Radius.zero,
bottomLeft: enableLeftBorder ? radius : Radius.zero,
topRight: enableRightBorder ? radius : Radius.zero,
bottomRight: enableRightBorder ? radius : Radius.zero,
topLeft: widget.enableLeftBorder ? radius : Radius.zero,
bottomLeft: widget.enableLeftBorder ? radius : Radius.zero,
topRight: widget.enableRightBorder ? radius : Radius.zero,
bottomRight: widget.enableRightBorder ? radius : Radius.zero,
),
);
}
@@ -538,7 +564,11 @@ class SimpleTableCellActionButtons extends ISimpleTableBottomSheetActions {
}
// close the action menu
Navigator.of(context).pop();
// keep the action menu open if the action is enable header row or enable header column
if (action != SimpleTableMoreAction.enableHeaderRow &&
action != SimpleTableMoreAction.enableHeaderColumn) {
Navigator.of(context).pop();
}
}
}
@@ -593,7 +623,7 @@ class _SimpleTableHeaderActionButtonState
child: CupertinoSwitch(
value: value,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (_) {},
onChanged: (_) => _toggle(),
),
),
);
@@ -1199,19 +1229,12 @@ class SimpleTableQuickActions extends StatelessWidget {
SimpleTableMoreAction.copy,
),
),
FutureBuilder(
future: getIt<ClipboardService>().getData(),
builder: (context, snapshot) {
final hasContent = snapshot.data?.tableJson != null;
return SimpleTableQuickAction(
type: SimpleTableMoreAction.paste,
isEnabled: hasContent,
onTap: () => _onActionTap(
context,
SimpleTableMoreAction.paste,
),
);
},
SimpleTableQuickAction(
type: SimpleTableMoreAction.paste,
onTap: () => _onActionTap(
context,
SimpleTableMoreAction.paste,
),
),
SimpleTableQuickAction(
type: SimpleTableMoreAction.delete,
@@ -199,8 +199,10 @@ class _SimpleTableMobileReorderButtonState
// show the bottom sheet
await showMobileBottomSheet(
context,
useSafeArea: false,
showDragHandle: true,
showDivider: false,
enablePadding: false,
builder: (context) => Provider.value(
value: simpleTableContext,
child: SimpleTableCellBottomSheet(
@@ -42,7 +42,12 @@ class SimpleTableAddColumnAndRowHoverButton extends StatelessWidget {
SimpleTableConstants.addColumnAndRowButtonBottomPadding,
right: SimpleTableConstants.addColumnButtonPadding,
child: SimpleTableAddColumnAndRowButton(
onTap: () => editorState.addColumnAndRowInTable(node),
onTap: () {
// cancel the selection to avoid flashing the selection
editorState.selection = null;
editorState.addColumnAndRowInTable(node);
},
),
)
: const SizedBox.shrink();
@@ -81,8 +81,12 @@ class _SimpleTableAddColumnHoverButtonState
child: Opacity(
opacity: shouldShow ? 1.0 : 0.0,
child: SimpleTableAddColumnButton(
onTap: () =>
widget.editorState.addColumnInTable(widget.tableNode),
onTap: () {
// cancel the selection to avoid flashing the selection
widget.editorState.selection = null;
widget.editorState.addColumnInTable(widget.tableNode);
},
onHorizontalDragStart: (details) {
context.read<SimpleTableContext>().isDraggingColumn = true;
startDraggingOffset = details.globalPosition;
@@ -87,9 +87,14 @@ class _SimpleTableAddRowHoverButtonState
SimpleTableConstants.cellBorderWidth,
right: SimpleTableConstants.addRowButtonRightPadding,
child: SimpleTableAddRowButton(
onTap: () => widget.editorState.addRowInTable(
widget.tableNode,
),
onTap: () {
// cancel the selection to avoid flashing the selection
widget.editorState.selection = null;
widget.editorState.addRowInTable(
widget.tableNode,
);
},
onVerticalDragStart: (details) {
context.read<SimpleTableContext>().isDraggingRow = true;
startDraggingOffset = details.globalPosition;
@@ -28,11 +28,13 @@ class SimpleTableCellBottomSheet extends StatefulWidget {
required this.type,
required this.cellNode,
required this.editorState,
this.scrollController,
});
final SimpleTableMoreActionType type;
final Node cellNode;
final EditorState editorState;
final ScrollController? scrollController;
@override
State<SimpleTableCellBottomSheet> createState() =>
@@ -81,7 +83,9 @@ class _SimpleTableCellBottomSheetState
_buildHeader(),
// content
..._buildContent(),
...menuState == _SimpleTableBottomSheetMenuState.cellActionMenu
? _buildScrollableContent()
: _buildNonScrollableContent(),
],
);
}
@@ -102,18 +106,51 @@ class _SimpleTableCellBottomSheetState
return BottomSheetHeader(
showBackButton: false,
showCloseButton: true,
showDoneButton: false,
showDoneButton: true,
showRemoveButton: false,
title: widget.type.name.capitalize(),
onClose: () => setState(() {
menuState = _SimpleTableBottomSheetMenuState.cellActionMenu;
}),
onDone: (_) => Navigator.pop(context),
);
default:
throw UnimplementedError('Unsupported menu state: $menuState');
}
}
List<Widget> _buildScrollableContent() {
return [
SizedBox(
height: SimpleTableConstants.actionSheetBottomSheetHeight,
child: Scrollbar(
controller: widget.scrollController,
child: SingleChildScrollView(
controller: widget.scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildContent(),
// safe area padding
VSpace(context.bottomSheetPadding() * 2),
],
),
),
),
),
];
}
List<Widget> _buildNonScrollableContent() {
return [
..._buildContent(),
// safe area padding
VSpace(context.bottomSheetPadding()),
];
}
List<Widget> _buildContent() {
switch (menuState) {
case _SimpleTableBottomSheetMenuState.cellActionMenu:
@@ -296,10 +333,12 @@ class SimpleTableBottomSheet extends StatefulWidget {
super.key,
required this.tableNode,
required this.editorState,
this.scrollController,
});
final Node tableNode;
final EditorState editorState;
final ScrollController? scrollController;
@override
State<SimpleTableBottomSheet> createState() => _SimpleTableBottomSheetState();
@@ -321,7 +360,24 @@ class _SimpleTableBottomSheetState extends State<SimpleTableBottomSheet> {
_buildHeader(),
// content
..._buildContent(),
SizedBox(
height: SimpleTableConstants.actionSheetBottomSheetHeight,
child: Scrollbar(
controller: widget.scrollController,
child: SingleChildScrollView(
controller: widget.scrollController,
child: Column(
children: [
// content
..._buildContent(),
// safe area padding
VSpace(context.bottomSheetPadding() * 2),
],
),
),
),
),
],
);
}
@@ -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() {
@@ -108,11 +108,18 @@ Future<bool> _showSlashMenu(
singleColumn: singleColumn,
style: style,
);
// disable the keyboard service
editorState.service.keyboardService?.disable();
if (!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST')) {
await _selectionMenuService?.show();
} else {
await _selectionMenuService?.show();
}
// enable the keyboard service
editorState.service.keyboardService?.enable();
}
if (shouldInsertSlash) {
@@ -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;
},
);
@@ -312,7 +312,9 @@ class _ToggleListBlockComponentWidgetState
placeholderText: placeholderText,
lineHeight: 1.5,
textSpanDecorator: (textSpan) {
var result = textSpan.updateTextStyle(textStyle);
var result = textSpan.updateTextStyle(
textStyleWithTextSpan(textSpan: textSpan),
);
if (level != null) {
result = result.updateTextStyle(
widget.textStyleBuilder?.call(level),
@@ -321,13 +323,17 @@ class _ToggleListBlockComponentWidgetState
return result;
},
placeholderTextSpanDecorator: (textSpan) {
var result = textSpan.updateTextStyle(textStyle);
var result = textSpan.updateTextStyle(
textStyleWithTextSpan(textSpan: textSpan),
);
if (level != null && widget.textStyleBuilder != null) {
result = result.updateTextStyle(
widget.textStyleBuilder?.call(level),
);
}
return result.updateTextStyle(placeholderTextStyle);
return result.updateTextStyle(
placeholderTextStyleWithTextSpan(textSpan: textSpan),
);
},
textDirection: textDirection,
textAlign: alignment?.toTextAlign ?? textAlign,
@@ -166,9 +166,10 @@ class EditorStyleCustomizer {
applyHeightToLastDescent: true,
),
textSpanDecorator: customizeAttributeDecorator,
mobileDragHandleBallSize: const Size.square(12.0),
magnifierSize: const Size(144, 96),
textScaleFactor: textScaleFactor,
mobileDragHandleLeftExtend: 12.0,
mobileDragHandleWidthExtend: 24.0,
);
}
@@ -138,22 +138,36 @@ class DateReferenceService extends InlineActionsDelegate {
final tomorrow = today.add(const Duration(days: 1));
final yesterday = today.subtract(const Duration(days: 1));
_allOptions = [
_itemFromDate(
late InlineActionsMenuItem todayItem;
late InlineActionsMenuItem tomorrowItem;
late InlineActionsMenuItem yesterdayItem;
try {
todayItem = _itemFromDate(
today,
LocaleKeys.relativeDates_today.tr(),
[DateFormat.yMd(_locale).format(today)],
),
_itemFromDate(
);
tomorrowItem = _itemFromDate(
tomorrow,
LocaleKeys.relativeDates_tomorrow.tr(),
[DateFormat.yMd(_locale).format(tomorrow)],
),
_itemFromDate(
);
yesterdayItem = _itemFromDate(
yesterday,
LocaleKeys.relativeDates_yesterday.tr(),
[DateFormat.yMd(_locale).format(yesterday)],
),
);
} catch (e) {
todayItem = _itemFromDate(today);
tomorrowItem = _itemFromDate(tomorrow);
yesterdayItem = _itemFromDate(yesterday);
}
_allOptions = [
todayItem,
tomorrowItem,
yesterdayItem,
];
}
@@ -173,7 +187,17 @@ class DateReferenceService extends InlineActionsDelegate {
String? label,
List<String>? keywords,
]) {
final labelStr = label ?? DateFormat.yMd(_locale).format(date);
late String labelStr;
if (label != null) {
labelStr = label;
} else {
try {
labelStr = DateFormat.yMd(_locale).format(date);
} catch (e) {
// fallback to en-US
labelStr = DateFormat.yMd('en-US').format(date);
}
}
return InlineActionsMenuItem(
label: labelStr.capitalize(),
@@ -170,17 +170,32 @@ class ReminderReferenceService extends InlineActionsDelegate {
final tomorrow = today.add(const Duration(days: 1));
final oneWeek = today.add(const Duration(days: 7));
_allOptions = [
_itemFromDate(
late InlineActionsMenuItem todayItem;
late InlineActionsMenuItem oneWeekItem;
try {
todayItem = _itemFromDate(
tomorrow,
LocaleKeys.relativeDates_tomorrow.tr(),
[DateFormat.yMd(_locale).format(tomorrow)],
),
_itemFromDate(
);
} catch (e) {
todayItem = _itemFromDate(today);
}
try {
oneWeekItem = _itemFromDate(
oneWeek,
LocaleKeys.relativeDates_oneWeek.tr(),
[DateFormat.yMd(_locale).format(oneWeek)],
),
);
} catch (e) {
oneWeekItem = _itemFromDate(oneWeek);
}
_allOptions = [
todayItem,
oneWeekItem,
];
}
@@ -200,7 +215,17 @@ class ReminderReferenceService extends InlineActionsDelegate {
String? label,
List<String>? keywords,
]) {
final labelStr = label ?? DateFormat.yMd(_locale).format(date);
late String labelStr;
if (label != null) {
labelStr = label;
} else {
try {
labelStr = DateFormat.yMd(_locale).format(date);
} catch (e) {
// fallback to en-US
labelStr = DateFormat.yMd('en-US').format(date);
}
}
return InlineActionsMenuItem(
label: labelStr.capitalize(),
@@ -34,6 +34,7 @@ class FlowyEmojiSearchBar extends StatefulWidget {
class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
final TextEditingController controller = TextEditingController();
EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none;
@override
void dispose() {
@@ -58,12 +59,18 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
),
const HSpace(8.0),
_RandomEmojiButton(
skinTone: skinTone,
emojiData: widget.emojiData,
onRandomEmojiSelected: widget.onRandomEmojiSelected,
),
const HSpace(8.0),
FlowyEmojiSkinToneSelector(
onEmojiSkinToneChanged: widget.onSkinToneChanged,
onEmojiSkinToneChanged: (v) {
setState(() {
skinTone = v;
});
widget.onSkinToneChanged.call(v);
},
),
],
),
@@ -73,10 +80,12 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
class _RandomEmojiButton extends StatelessWidget {
const _RandomEmojiButton({
required this.skinTone,
required this.emojiData,
required this.onRandomEmojiSelected,
});
final EmojiSkinTone skinTone;
final EmojiData emojiData;
final EmojiSelectedCallback onRandomEmojiSelected;
@@ -100,9 +109,14 @@ class _RandomEmojiButton extends StatelessWidget {
),
onTap: () {
final random = emojiData.random;
final emojiId = random.$1;
final emoji = emojiData.getEmojiById(
emojiId,
skinTone: skinTone,
);
onRandomEmojiSelected(
random.$1,
random.$2,
emojiId,
emoji,
);
},
),
@@ -131,6 +145,9 @@ class _SearchTextFieldState extends State<_SearchTextField> {
@override
void initState() {
super.initState();
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
/// this is to ensure that focus can be regained within a short period of time
if (widget.ensureFocus) {
Future.delayed(const Duration(milliseconds: 200), () {
if (!mounted || focusNode.hasFocus) return;
@@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart' hide Icon;
import 'package:flutter/services.dart';
import 'package:universal_platform/universal_platform.dart';
extension ToProto on FlowyIconType {
@@ -46,6 +47,10 @@ enum FlowyIconType {
custom;
}
extension FlowyIconTypeToPickerTabType on FlowyIconType {
PickerTabType? toPickerTabType() => name.toPickerTabType();
}
class EmojiIconData {
factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, '');
@@ -78,17 +83,35 @@ class EmojiIconData {
bool get isNotEmpty => emoji.isNotEmpty;
}
class SelectedEmojiIconResult {
SelectedEmojiIconResult(this.data, this.keepOpen);
final EmojiIconData data;
final bool keepOpen;
FlowyIconType get type => data.type;
String get emoji => data.emoji;
}
extension EmojiIconDataToSelectedResultExtension on EmojiIconData {
SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) =>
SelectedEmojiIconResult(this, keepOpen);
}
class FlowyIconEmojiPicker extends StatefulWidget {
const FlowyIconEmojiPicker({
super.key,
this.onSelectedEmoji,
this.initialType,
this.enableBackgroundColorSelection = true,
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
});
final ValueChanged<EmojiIconData>? onSelectedEmoji;
final ValueChanged<SelectedEmojiIconResult>? onSelectedEmoji;
final bool enableBackgroundColorSelection;
final List<PickerTabType> tabs;
final PickerTabType? initialType;
@override
State<FlowyIconEmojiPicker> createState() => _FlowyIconEmojiPickerState();
@@ -96,12 +119,23 @@ class FlowyIconEmojiPicker extends StatefulWidget {
class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
with SingleTickerProviderStateMixin {
late final controller = TabController(
length: widget.tabs.length,
vsync: this,
);
late TabController controller;
int currentIndex = 0;
@override
void initState() {
super.initState();
final initialType = widget.initialType;
if (initialType != null) {
currentIndex = widget.tabs.indexOf(initialType);
}
controller = TabController(
initialIndex: currentIndex,
length: widget.tabs.length,
vsync: this,
);
}
@override
void dispose() {
controller.dispose();
@@ -127,7 +161,8 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
),
_RemoveIconButton(
onTap: () {
widget.onSelectedEmoji?.call(EmojiIconData.none());
widget.onSelectedEmoji
?.call(EmojiIconData.none().toSelectedResult());
},
),
],
@@ -155,9 +190,12 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
return FlowyEmojiPicker(
ensureFocus: true,
emojiPerLine: _getEmojiPerLine(context),
onEmojiSelected: (_, emoji) => widget.onSelectedEmoji?.call(
EmojiIconData.emoji(emoji),
),
onEmojiSelected: (r) {
widget.onSelectedEmoji?.call(
EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom),
);
SystemChannels.textInput.invokeMethod('TextInput.hide');
},
);
}
@@ -171,9 +209,13 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
Widget _buildIconPicker() {
return FlowyIconPicker(
ensureFocus: true,
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
onSelectedIcon: (result) {
widget.onSelectedEmoji?.call(result.toEmojiIconData());
onSelectedIcon: (r) {
widget.onSelectedEmoji?.call(
r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom),
);
SystemChannels.textInput.invokeMethod('TextInput.hide');
},
);
}
@@ -39,8 +39,9 @@ class IconGroup {
final filteredIcons = icons
.where(
(icon) =>
icon.keywords.any((k) => k.contains(lowercaseKey)) ||
icon.name.contains(lowercaseKey),
icon.keywords
.any((k) => k.toLowerCase().contains(lowercaseKey)) ||
icon.name.toLowerCase().contains(lowercaseKey),
)
.toList();
return IconGroup(name: name, icons: filteredIcons);
@@ -84,3 +85,23 @@ class Icon {
return '${iconGroup!.name}/$name';
}
}
class RecentIcon {
factory RecentIcon.fromJson(Map<String, dynamic> json) =>
RecentIcon(_$IconFromJson(json), json['groupName'] ?? '');
RecentIcon(this.icon, this.groupName);
final Icon icon;
final String groupName;
String get name => icon.name;
List<String> get keywords => icon.keywords;
String get content => icon.content;
Map<String, dynamic> toJson() => _$IconToJson(
Icon(name: name, keywords: keywords, content: content),
)..addAll({'groupName': groupName});
}
@@ -75,17 +75,31 @@ Future<List<IconGroup>> loadIconGroups() async {
}
}
class IconPickerResult {
IconPickerResult(this.data, this.isRandom);
final IconsData data;
final bool isRandom;
}
extension IconsDataToIconPickerResultExtension on IconsData {
IconPickerResult toResult({bool isRandom = false}) =>
IconPickerResult(this, isRandom);
}
class FlowyIconPicker extends StatefulWidget {
const FlowyIconPicker({
super.key,
required this.onSelectedIcon,
required this.enableBackgroundColorSelection,
this.iconPerLine = 9,
this.ensureFocus = false,
});
final bool enableBackgroundColorSelection;
final ValueChanged<IconsData> onSelectedIcon;
final ValueChanged<IconPickerResult> onSelectedIcon;
final int iconPerLine;
final bool ensureFocus;
@override
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
@@ -104,10 +118,13 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
iconGroups.add(
IconGroup(
name: _kRecentIconGroupName,
icons: recentIcons.sublist(
0,
min(recentIcons.length, widget.iconPerLine),
),
icons: recentIcons
.sublist(
0,
min(recentIcons.length, widget.iconPerLine),
)
.map((e) => e.icon)
.toList(),
),
);
}
@@ -142,6 +159,7 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: IconSearchBar(
ensureFocus: widget.ensureFocus,
onRandomTap: () {
final value = kIconGroups?.randomIcon();
if (value == null) {
@@ -154,8 +172,9 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
value.$2.content,
value.$2.name,
color,
),
).toResult(isRandom: true),
);
RecentIcons.putIcon(RecentIcon(value.$2, value.$1.name));
},
onKeywordChanged: (keyword) => {
debounce.call(() {
@@ -193,14 +212,14 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
iconGroups: filteredIconGroups,
enableBackgroundColorSelection:
widget.enableBackgroundColorSelection,
onSelectedIcon: widget.onSelectedIcon,
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
iconPerLine: widget.iconPerLine,
);
}
return IconPicker(
iconGroups: iconGroups,
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
onSelectedIcon: widget.onSelectedIcon,
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
iconPerLine: widget.iconPerLine,
);
},
@@ -278,6 +297,7 @@ class _IconPickerState extends State<IconPicker> {
crossAxisCount: widget.iconPerLine,
),
itemCount: iconGroup.icons.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
final icon = iconGroup.icons[index];
@@ -286,30 +306,38 @@ class _IconPickerState extends State<IconPicker> {
icon: icon,
mutex: mutex,
onSelectedColor: (context, color) {
String groupName = iconGroup.name;
if (groupName == _kRecentIconGroupName) {
groupName = getGroupName(index);
}
widget.onSelectedIcon(
IconsData(
iconGroup.name,
groupName,
icon.content,
icon.name,
color,
),
);
RecentIcons.putIcon(icon);
RecentIcons.putIcon(RecentIcon(icon, groupName));
PopoverContainer.of(context).close();
},
)
: _IconNoBackground(
icon: icon,
onSelectedIcon: () {
String groupName = iconGroup.name;
if (groupName == _kRecentIconGroupName) {
groupName = getGroupName(index);
}
widget.onSelectedIcon(
IconsData(
iconGroup.name,
groupName,
icon.content,
icon.name,
null,
),
);
RecentIcons.putIcon(icon);
RecentIcons.putIcon(RecentIcon(icon, groupName));
},
);
},
@@ -324,6 +352,16 @@ class _IconPickerState extends State<IconPicker> {
},
);
}
String getGroupName(int index) {
final recentIcons = RecentIcons.getIconsSync();
try {
return recentIcons[index].groupName;
} catch (e) {
Log.error('getGroupName with index: $index error', e);
return '';
}
}
}
class _IconNoBackground extends StatelessWidget {
@@ -375,12 +413,20 @@ class _Icon extends StatefulWidget {
class _IconState extends State<_Icon> {
final PopoverController _popoverController = PopoverController();
@override
void dispose() {
super.dispose();
_popoverController.close();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 6),
mutex: widget.mutex,
clickHandler: PopoverClickHandler.gestureDetector,
child: _IconNoBackground(
icon: widget.icon,
onSelectedIcon: () => _popoverController.show(),
@@ -16,9 +16,11 @@ class IconSearchBar extends StatefulWidget {
super.key,
required this.onRandomTap,
required this.onKeywordChanged,
this.ensureFocus = false,
});
final VoidCallback onRandomTap;
final bool ensureFocus;
final IconKeywordChangedCallback onKeywordChanged;
@override
@@ -46,6 +48,7 @@ class _IconSearchBarState extends State<IconSearchBar> {
Expanded(
child: _SearchTextField(
onKeywordChanged: widget.onKeywordChanged,
ensureFocus: widget.ensureFocus,
),
),
const HSpace(8.0),
@@ -93,9 +96,11 @@ class _RandomIconButton extends StatelessWidget {
class _SearchTextField extends StatefulWidget {
const _SearchTextField({
required this.onKeywordChanged,
this.ensureFocus = false,
});
final IconKeywordChangedCallback onKeywordChanged;
final bool ensureFocus;
@override
State<_SearchTextField> createState() => _SearchTextFieldState();
@@ -105,6 +110,20 @@ class _SearchTextFieldState extends State<_SearchTextField> {
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
/// this is to ensure that focus can be regained within a short period of time
if (widget.ensureFocus) {
Future.delayed(const Duration(milliseconds: 200), () {
if (!mounted || focusNode.hasFocus) return;
focusNode.requestFocus();
});
}
}
@override
void dispose() {
controller.dispose();
@@ -22,13 +22,10 @@ class RecentIcons {
await _put(FlowyIconType.emoji, id);
}
static Future<void> putIcon(Icon icon) async {
static Future<void> putIcon(RecentIcon icon) async {
await _put(
FlowyIconType.icon,
jsonEncode(
Icon(name: icon.name, keywords: icon.keywords, content: icon.content)
.toJson(),
),
jsonEncode(icon.toJson()),
);
}
@@ -37,12 +34,22 @@ class RecentIcons {
return _dataMap[FlowyIconType.emoji.name] ?? [];
}
static Future<List<Icon>> getIcons() async {
static Future<List<RecentIcon>> getIcons() async {
await _load();
return getIconsSync();
}
static List<RecentIcon> getIconsSync() {
final iconList = _dataMap[FlowyIconType.icon.name] ?? [];
try {
return iconList
.map((e) => Icon.fromJson(jsonDecode(e) as Map<String, dynamic>))
.map(
(e) => RecentIcon.fromJson(jsonDecode(e) as Map<String, dynamic>),
)
/// skip the data that is already stored locally but has an empty
/// groupName to accommodate the issue of destructive data modifications
.skipWhile((e) => e.groupName.isEmpty)
.toList();
} catch (e) {
Log.error('RecentIcons getIcons with :$iconList', e);
@@ -15,6 +15,16 @@ enum PickerTabType {
}
}
extension StringToPickerTabType on String {
PickerTabType? toPickerTabType() {
try {
return PickerTabType.values.byName(this);
} on ArgumentError {
return null;
}
}
}
class PickerTab extends StatelessWidget {
const PickerTab({
super.key,
@@ -29,6 +29,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter/foundation.dart';
@@ -285,14 +286,26 @@ GoRoute _mobileEmojiPickerPageRoute() {
state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle];
final selectTabs =
state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? '';
final tabs = selectTabs
.split('-')
.map((e) => PickerTabType.values.byName(e))
.toList();
final selectedType = state
.uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType]
?.toPickerTabType();
List<PickerTabType> tabs = [];
try {
tabs = selectTabs
.split('-')
.map((e) => PickerTabType.values.byName(e))
.toList();
} on ArgumentError catch (e) {
Log.error('convert selectTabs to pickerTab error', e);
}
return MaterialExtendedPage(
child: tabs.isEmpty
? MobileEmojiPickerScreen(title: title)
: MobileEmojiPickerScreen(title: title, tabs: tabs),
? MobileEmojiPickerScreen(title: title, selectedType: selectedType)
: MobileEmojiPickerScreen(
title: title,
selectedType: selectedType,
tabs: tabs,
),
);
},
);
@@ -1,4 +1,5 @@
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
@@ -48,10 +49,17 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
emit(state.copyWith(isLoading: e.isLoading));
},
didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {
// the latest view is shared across all the members of the workspace.
final latestView = value.setting.hasLatestView()
? value.setting.latestView
: state.latestView;
if (latestView != null && latestView.isSpace) {
// If the latest view is a space, we don't need to open it.
return;
}
emit(
state.copyWith(
workspaceSetting: value.setting,
@@ -8,7 +8,14 @@ const _friendlyFmt = 'MMM dd, y';
const _dmyFmt = 'dd/MM/y';
extension DateFormatter on UserDateFormatPB {
DateFormat get toFormat => DateFormat(_toFormat[this] ?? _friendlyFmt);
DateFormat get toFormat {
try {
return DateFormat(_toFormat[this] ?? _friendlyFmt);
} catch (_) {
// fallback to en-US
return DateFormat(_toFormat[this] ?? _friendlyFmt, 'en-US');
}
}
String formatDate(
DateTime date,
Loaded 100 of 553 files, more files were not shown because too many files have changed in this diff. Show more