mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-22 21:57:16 -04:00
Compare commits
21 commits
Author | SHA1 | Date | |
---|---|---|---|
|
627b8ab43c | ||
|
84d55b5022 | ||
|
b53e20f746 | ||
|
cd06161dea | ||
|
776f70a0c7 | ||
|
1d1647d58d | ||
|
b3f94be33f | ||
|
ace398537b | ||
|
430e21aec2 | ||
|
0c1eb7306a | ||
|
db11886e5f | ||
|
0d13336b32 | ||
|
60ad397105 | ||
|
fca3189c97 | ||
|
e63f767926 | ||
|
8826e479eb | ||
|
8e4fe3d559 | ||
|
e2ee11e48a | ||
|
bdc0fa1f2a | ||
|
3836545682 | ||
|
02eb0e0b83 |
101 changed files with 2651 additions and 630 deletions
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -1,8 +1,40 @@
|
|||
# Release Notes
|
||||
## Version 0.7.9 - 25/12/2024
|
||||
### New Features
|
||||
|
||||
## Version 0.8.0 - 06/01/2025
|
||||
### Bug Fixes
|
||||
Fixed error displaying in the page style menu
|
||||
Fixed filter logic in the icon picker
|
||||
Fixed error displaying in the Favorite/Recent page
|
||||
Fixed the color picker displaying when tapping down
|
||||
Fixed icons not being supported in subpage blocks
|
||||
Fixed recent icon functionality in the space icon menu
|
||||
Fixed "Insert Below" not auto-scrolling the table
|
||||
Fixed a to-do item with an emoji automatically creating a soft break
|
||||
Fixed header row/column tap areas being too small
|
||||
Fixed simple table alignment not working for items that wrap
|
||||
Fixed web content reverting after removing the inline code format on desktop
|
||||
Fixed inability to make changes to a row or column in the table when opening a new tab
|
||||
Fixed changing the language to CKB-KU causing a gray screen on mobile
|
||||
|
||||
## Version 0.7.9 - 30/12/2024
|
||||
### New Features
|
||||
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
|
||||
- Create beautiful documents with 22 content types and markdown support
|
||||
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
|
||||
- Invite members to your workspace for seamless collaboration
|
||||
- Create multiple public/private spaces to better organize your content
|
||||
- Simple Table is now available on Mobile, designed specifically for mobile devices.
|
||||
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
|
||||
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
|
||||
- Use '/' to insert a content block into a table cell on Desktop
|
||||
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
|
||||
- Add messages to an editable document while chatting with AI side by side
|
||||
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
|
||||
- Drag a page from the sidebar into a document to easily mention the page without typing its title
|
||||
- Paste as plain text, a new option in the right-click paste menu
|
||||
### Bug Fixes
|
||||
- Fixed misalignment in numbered lists
|
||||
- Resolved several bugs in the emoji menu
|
||||
- Fixed a bug with checklist items
|
||||
|
||||
## Version 0.7.8 - 18/12/2024
|
||||
### New Features
|
||||
|
|
|
@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
|||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.7.9"
|
||||
APPFLOWY_VERSION = "0.8.0"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
|
|
@ -53,7 +53,7 @@ android {
|
|||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "io.appflowy.appflowy"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'data_migration/data_migration_test_runner.dart'
|
|||
as data_migration_test_runner;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'set_env.dart' as preset_af_cloud_env_test;
|
||||
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
|
||||
import 'sidebar/sidebar_rename_untitled_test.dart'
|
||||
as sidebar_rename_untitled_test;
|
||||
|
@ -26,4 +27,5 @@ Future<void> main() async {
|
|||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -274,7 +274,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
|||
onPressed: () async {
|
||||
final documentId = getOpenedDocumentId();
|
||||
if (documentId != null) {
|
||||
await onAddToExistingPage(documentId);
|
||||
await onAddToExistingPage(context, documentId);
|
||||
await forceReloadAndUpdateSelection(documentId);
|
||||
} else {
|
||||
widget.onOverrideVisibility?.call(true);
|
||||
|
@ -298,9 +298,8 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
|||
},
|
||||
onAddToExistingPage: (documentId) async {
|
||||
popoverController.close();
|
||||
await onAddToExistingPage(documentId);
|
||||
final view =
|
||||
await ViewBackendService.getView(documentId).toNullable();
|
||||
final view = await onAddToExistingPage(context, documentId);
|
||||
|
||||
if (context.mounted) {
|
||||
openPageFromMessage(context, view);
|
||||
}
|
||||
|
@ -309,12 +308,20 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> onAddToExistingPage(String documentId) async {
|
||||
Future<ViewPB?> onAddToExistingPage(
|
||||
BuildContext context,
|
||||
String documentId,
|
||||
) async {
|
||||
await ChatEditDocumentService.addMessageToPage(
|
||||
documentId,
|
||||
widget.textMessage,
|
||||
);
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
final view = await ViewBackendService.getView(documentId).toNullable();
|
||||
if (context.mounted) {
|
||||
showSaveMessageSuccessToast(context, view);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
void addMessageToNewPage(BuildContext context) async {
|
||||
|
@ -327,12 +334,43 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
|||
chatView.parentViewId,
|
||||
[widget.textMessage],
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
showSaveMessageSuccessToast(context, newView);
|
||||
openPageFromMessage(context, newView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
showToastNotification(
|
||||
context,
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: LocaleKeys.chat_addToNewPageSuccessToast.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' ',
|
||||
),
|
||||
TextSpan(
|
||||
text: view.nameOrDefault,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFFFFFFFF),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> forceReloadAndUpdateSelection(String documentId) async {
|
||||
final bloc = DocumentBloc.findOpen(documentId);
|
||||
if (bloc == null) {
|
||||
|
|
|
@ -90,6 +90,7 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
|
|||
data == null) {
|
||||
return _MetadataButton(
|
||||
name: m.name,
|
||||
onTap: () => widget.onSelectedMetadata?.call(m),
|
||||
);
|
||||
}
|
||||
return BlocProvider(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,10 +534,11 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
|
|||
) {
|
||||
return HeadingBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (node) => _buildTextStyleInTableCell(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
padding: (node) {
|
||||
if (customHeadingPadding != null) {
|
||||
|
@ -670,7 +676,12 @@ DatabaseViewBlockComponentBuilder _buildDatabaseViewBlockComponentBuilder(
|
|||
) {
|
||||
return DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return configuration.padding(node);
|
||||
}
|
||||
return const EdgeInsets.symmetric(vertical: 10);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -693,10 +704,11 @@ CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder(
|
|||
node: node,
|
||||
configuration: configuration,
|
||||
),
|
||||
textStyle: (node) => _buildTextStyleInTableCell(
|
||||
textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
),
|
||||
),
|
||||
inlinePadding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -784,11 +796,12 @@ ToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder(
|
|||
|
||||
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
|
||||
},
|
||||
textStyle: (node) {
|
||||
textStyle: (node, {TextSpan? textSpan}) {
|
||||
final textStyle = _buildTextStyleInTableCell(
|
||||
context,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
textSpan: textSpan,
|
||||
);
|
||||
final level = node.attributes[ToggleListBlockKeys.level] as int?;
|
||||
if (level == null) {
|
||||
|
@ -823,9 +836,14 @@ OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder(
|
|||
) {
|
||||
return OutlineBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderTextStyle: (_) =>
|
||||
placeholderTextStyle: (node, {TextSpan? textSpan}) =>
|
||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return configuration.padding(node);
|
||||
}
|
||||
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -836,7 +854,12 @@ LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(
|
|||
) {
|
||||
return LinkPreviewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return configuration.padding(node);
|
||||
}
|
||||
return const EdgeInsets.symmetric(vertical: 10);
|
||||
},
|
||||
),
|
||||
cache: LinkPreviewDataCache(),
|
||||
showMenu: true,
|
||||
|
@ -872,7 +895,14 @@ SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder(
|
|||
}) {
|
||||
return SubPageBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (node) => styleCustomizer.subPageBlockTextStyleBuilder(),
|
||||
textStyle: (node, {TextSpan? textSpan}) =>
|
||||
styleCustomizer.subPageBlockTextStyleBuilder(),
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return const EdgeInsets.symmetric(horizontal: 18);
|
||||
}
|
||||
return configuration.padding(node);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -881,8 +911,9 @@ TextStyle _buildTextStyleInTableCell(
|
|||
BuildContext context, {
|
||||
required Node node,
|
||||
required BlockComponentConfiguration configuration,
|
||||
required TextSpan? textSpan,
|
||||
}) {
|
||||
TextStyle textStyle = configuration.textStyle(node);
|
||||
TextStyle textStyle = configuration.textStyle(node, textSpan: textSpan);
|
||||
|
||||
if (node.isInHeaderColumn ||
|
||||
node.isInHeaderRow ||
|
||||
|
@ -895,6 +926,11 @@ TextStyle _buildTextStyleInTableCell(
|
|||
|
||||
final cellTextColor = node.textColorInColumn ?? node.textColorInRow;
|
||||
|
||||
// enable it if we need to support the text color of the text span
|
||||
// final isTextSpanColorNull = textSpan?.style?.color == null;
|
||||
// final isTextSpanChildrenColorNull =
|
||||
// textSpan?.children?.every((e) => e.style?.color == null) ?? true;
|
||||
|
||||
if (cellTextColor != null) {
|
||||
textStyle = textStyle.copyWith(
|
||||
color: buildEditorCustomizedColor(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -239,18 +239,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
|
|||
SimpleTableInsertAction(
|
||||
type: SimpleTableMoreAction.insertAbove,
|
||||
enableLeftBorder: true,
|
||||
onTap: () => _onActionTap(
|
||||
onTap: (increaseCounter) async => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.insertAbove,
|
||||
type: SimpleTableMoreAction.insertAbove,
|
||||
increaseCounter: increaseCounter,
|
||||
),
|
||||
),
|
||||
const HSpace(2),
|
||||
SimpleTableInsertAction(
|
||||
type: SimpleTableMoreAction.insertBelow,
|
||||
enableRightBorder: true,
|
||||
onTap: () => _onActionTap(
|
||||
onTap: (increaseCounter) async => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.insertBelow,
|
||||
type: SimpleTableMoreAction.insertBelow,
|
||||
increaseCounter: increaseCounter,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -260,18 +262,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
|
|||
SimpleTableInsertAction(
|
||||
type: SimpleTableMoreAction.insertLeft,
|
||||
enableLeftBorder: true,
|
||||
onTap: () => _onActionTap(
|
||||
onTap: (increaseCounter) async => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.insertLeft,
|
||||
type: SimpleTableMoreAction.insertLeft,
|
||||
increaseCounter: increaseCounter,
|
||||
),
|
||||
),
|
||||
const HSpace(2),
|
||||
SimpleTableInsertAction(
|
||||
type: SimpleTableMoreAction.insertRight,
|
||||
enableRightBorder: true,
|
||||
onTap: () => _onActionTap(
|
||||
onTap: (increaseCounter) async => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.insertRight,
|
||||
type: SimpleTableMoreAction.insertRight,
|
||||
increaseCounter: increaseCounter,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -279,7 +283,11 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
|
|||
};
|
||||
}
|
||||
|
||||
void _onActionTap(BuildContext context, SimpleTableMoreAction type) {
|
||||
Future<void> _onActionTap(
|
||||
BuildContext context, {
|
||||
required SimpleTableMoreAction type,
|
||||
required int increaseCounter,
|
||||
}) async {
|
||||
final simpleTableContext = context.read<SimpleTableContext>();
|
||||
final tableNode = cellNode.parentTableNode;
|
||||
if (tableNode == null) {
|
||||
|
@ -291,34 +299,48 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions {
|
|||
case SimpleTableMoreAction.insertAbove:
|
||||
// update the highlight status for the selecting row
|
||||
simpleTableContext.selectingRow.value = cellNode.rowIndex + 1;
|
||||
editorState.insertRowInTable(
|
||||
await editorState.insertRowInTable(
|
||||
tableNode,
|
||||
cellNode.rowIndex,
|
||||
);
|
||||
case SimpleTableMoreAction.insertBelow:
|
||||
editorState.insertRowInTable(
|
||||
await editorState.insertRowInTable(
|
||||
tableNode,
|
||||
cellNode.rowIndex + 1,
|
||||
);
|
||||
// scroll to the next cell position
|
||||
editorState.scrollService?.scrollTo(
|
||||
SimpleTableConstants.defaultRowHeight,
|
||||
duration: Durations.short3,
|
||||
);
|
||||
case SimpleTableMoreAction.insertLeft:
|
||||
// update the highlight status for the selecting column
|
||||
simpleTableContext.selectingColumn.value = cellNode.columnIndex + 1;
|
||||
editorState.insertColumnInTable(
|
||||
await editorState.insertColumnInTable(
|
||||
tableNode,
|
||||
cellNode.columnIndex,
|
||||
);
|
||||
case SimpleTableMoreAction.insertRight:
|
||||
editorState.insertColumnInTable(
|
||||
await editorState.insertColumnInTable(
|
||||
tableNode,
|
||||
cellNode.columnIndex + 1,
|
||||
);
|
||||
final horizontalScrollController =
|
||||
simpleTableContext.horizontalScrollController;
|
||||
if (horizontalScrollController != null) {
|
||||
final previousWidth = horizontalScrollController.offset;
|
||||
horizontalScrollController.jumpTo(
|
||||
previousWidth + SimpleTableConstants.defaultColumnWidth,
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
assert(false, 'Unsupported action: $type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTableInsertAction extends StatelessWidget {
|
||||
class SimpleTableInsertAction extends StatefulWidget {
|
||||
const SimpleTableInsertAction({
|
||||
super.key,
|
||||
required this.type,
|
||||
|
@ -330,7 +352,16 @@ class SimpleTableInsertAction extends StatelessWidget {
|
|||
final SimpleTableMoreAction type;
|
||||
final bool enableLeftBorder;
|
||||
final bool enableRightBorder;
|
||||
final void Function() onTap;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
@override
|
||||
State<SimpleTableInsertAction> createState() =>
|
||||
_SimpleTableInsertActionState();
|
||||
}
|
||||
|
||||
class _SimpleTableInsertActionState extends State<SimpleTableInsertAction> {
|
||||
// used to count how many times the action is tapped
|
||||
int increaseCounter = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -341,19 +372,19 @@ class SimpleTableInsertAction extends StatelessWidget {
|
|||
shape: _buildBorder(),
|
||||
),
|
||||
child: AnimatedGestureDetector(
|
||||
onTapUp: onTap,
|
||||
onTapUp: () => widget.onTap(increaseCounter++),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: FlowySvg(
|
||||
type.leftIconSvg,
|
||||
widget.type.leftIconSvg,
|
||||
size: const Size.square(22),
|
||||
),
|
||||
),
|
||||
FlowyText(
|
||||
type.name,
|
||||
widget.type.name,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
),
|
||||
|
@ -370,10 +401,10 @@ class SimpleTableInsertAction extends StatelessWidget {
|
|||
);
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: enableLeftBorder ? radius : Radius.zero,
|
||||
bottomLeft: enableLeftBorder ? radius : Radius.zero,
|
||||
topRight: enableRightBorder ? radius : Radius.zero,
|
||||
bottomRight: enableRightBorder ? radius : Radius.zero,
|
||||
topLeft: widget.enableLeftBorder ? radius : Radius.zero,
|
||||
bottomLeft: widget.enableLeftBorder ? radius : Radius.zero,
|
||||
topRight: widget.enableRightBorder ? radius : Radius.zero,
|
||||
bottomRight: widget.enableRightBorder ? radius : Radius.zero,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -592,7 +623,7 @@ class _SimpleTableHeaderActionButtonState
|
|||
child: CupertinoSwitch(
|
||||
value: value,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (_) {},
|
||||
onChanged: (_) => _toggle(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1198,19 +1229,12 @@ class SimpleTableQuickActions extends StatelessWidget {
|
|||
SimpleTableMoreAction.copy,
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: getIt<ClipboardService>().getData(),
|
||||
builder: (context, snapshot) {
|
||||
final hasContent = snapshot.data?.tableJson != null;
|
||||
return SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.paste,
|
||||
isEnabled: hasContent,
|
||||
onTap: () => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.paste,
|
||||
),
|
||||
);
|
||||
},
|
||||
SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.paste,
|
||||
onTap: () => _onActionTap(
|
||||
context,
|
||||
SimpleTableMoreAction.paste,
|
||||
),
|
||||
),
|
||||
SimpleTableQuickAction(
|
||||
type: SimpleTableMoreAction.delete,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -65,6 +65,10 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
|
|||
state.currentPageManager.hideSecondaryPlugin();
|
||||
emit(state.openPlugin(plugin: plugin, setLatest: setLatest));
|
||||
if (setLatest) {
|
||||
// the space view should be filtered out.
|
||||
if (view != null && view.isSpace) {
|
||||
return;
|
||||
}
|
||||
_setLatestOpenView(view);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,9 +2,6 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'package:appflowy/core/frameless_window.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
@ -24,6 +21,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:time/time.dart';
|
||||
|
@ -585,6 +584,10 @@ class PageManager {
|
|||
value: _notifier,
|
||||
child: Consumer<PageNotifier>(
|
||||
builder: (_, notifier, __) {
|
||||
if (notifier.plugin.pluginType == PluginType.blank) {
|
||||
return const BlankPage();
|
||||
}
|
||||
|
||||
return FadingIndexedStack(
|
||||
index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
|
||||
children: getIt<PluginSandbox>().supportPluginTypes.map(
|
||||
|
|
|
@ -173,21 +173,22 @@ class _SidebarSpaceHeaderState extends State<SidebarSpaceHeader> {
|
|||
await _showRenameDialog();
|
||||
break;
|
||||
case SpaceMoreActionType.changeIcon:
|
||||
final result = data as EmojiIconData;
|
||||
if (data.type == FlowyIconType.icon) {
|
||||
try {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(result.emoji));
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.changeIcon(
|
||||
icon: '${iconsData.groupName}/${iconsData.iconName}',
|
||||
iconColor: iconsData.color,
|
||||
),
|
||||
);
|
||||
} on FormatException catch (e) {
|
||||
context
|
||||
.read<SpaceBloc>()
|
||||
.add(const SpaceEvent.changeIcon(icon: ''));
|
||||
Log.warn('SidebarSpaceHeader changeIcon error:$e');
|
||||
if (data is SelectedEmojiIconResult) {
|
||||
if (data.type == FlowyIconType.icon) {
|
||||
try {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.changeIcon(
|
||||
icon: '${iconsData.groupName}/${iconsData.iconName}',
|
||||
iconColor: iconsData.color,
|
||||
),
|
||||
);
|
||||
} on FormatException catch (e) {
|
||||
context
|
||||
.read<SpaceBloc>()
|
||||
.add(const SpaceEvent.changeIcon(icon: ''));
|
||||
Log.warn('SidebarSpaceHeader changeIcon error:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -18,15 +18,23 @@ enum WorkspaceMoreAction {
|
|||
divider,
|
||||
}
|
||||
|
||||
class WorkspaceMoreActionList extends StatelessWidget {
|
||||
class WorkspaceMoreActionList extends StatefulWidget {
|
||||
const WorkspaceMoreActionList({
|
||||
super.key,
|
||||
required this.workspace,
|
||||
required this.isShowingMoreActions,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final UserWorkspacePB workspace;
|
||||
final ValueNotifier<bool> isShowingMoreActions;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
State<WorkspaceMoreActionList> createState() =>
|
||||
_WorkspaceMoreActionListState();
|
||||
}
|
||||
|
||||
class _WorkspaceMoreActionListState extends State<WorkspaceMoreActionList> {
|
||||
bool isPopoverOpen = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -45,16 +53,22 @@ class WorkspaceMoreActionList extends StatelessWidget {
|
|||
return PopoverActionList<_WorkspaceMoreActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
actions: actions
|
||||
.map((e) => _WorkspaceMoreActionWrapper(e, workspace))
|
||||
.map(
|
||||
(action) => _WorkspaceMoreActionWrapper(
|
||||
action,
|
||||
widget.workspace,
|
||||
() => PopoverContainer.of(context).closeAll(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mutex: widget.popoverMutex,
|
||||
constraints: const BoxConstraints(minWidth: 220),
|
||||
animationDuration: Durations.short3,
|
||||
slideDistance: 2,
|
||||
beginScaleFactor: 1.0,
|
||||
beginOpacity: 0.8,
|
||||
onClosed: () {
|
||||
isShowingMoreActions.value = false;
|
||||
},
|
||||
onClosed: () => isPopoverOpen = false,
|
||||
asBarrier: true,
|
||||
buildChild: (controller) {
|
||||
return SizedBox.square(
|
||||
dimension: 24.0,
|
||||
|
@ -64,11 +78,10 @@ class WorkspaceMoreActionList extends StatelessWidget {
|
|||
FlowySvgs.workspace_three_dots_s,
|
||||
),
|
||||
onTap: () {
|
||||
if (!isShowingMoreActions.value) {
|
||||
if (!isPopoverOpen) {
|
||||
controller.show();
|
||||
isPopoverOpen = true;
|
||||
}
|
||||
|
||||
isShowingMoreActions.value = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -79,10 +92,15 @@ class WorkspaceMoreActionList extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _WorkspaceMoreActionWrapper extends CustomActionCell {
|
||||
_WorkspaceMoreActionWrapper(this.inner, this.workspace);
|
||||
_WorkspaceMoreActionWrapper(
|
||||
this.inner,
|
||||
this.workspace,
|
||||
this.closeWorkspaceMenu,
|
||||
);
|
||||
|
||||
final WorkspaceMoreAction inner;
|
||||
final UserWorkspacePB workspace;
|
||||
final VoidCallback closeWorkspaceMenu;
|
||||
|
||||
@override
|
||||
Widget buildWithContext(
|
||||
|
@ -117,6 +135,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
|
|||
margin: const EdgeInsets.all(6),
|
||||
onTap: () async {
|
||||
PopoverContainer.of(context).closeAll();
|
||||
closeWorkspaceMenu();
|
||||
|
||||
final workspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
switch (inner) {
|
||||
|
|
|
@ -96,9 +96,9 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||
margin: const EdgeInsets.all(0),
|
||||
popupBuilder: (_) => FlowyIconEmojiPicker(
|
||||
tabs: const [PickerTabType.emoji],
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onSelected(result);
|
||||
controller.close();
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onSelected(r.data);
|
||||
if (!r.keepOpen) controller.close();
|
||||
},
|
||||
),
|
||||
child: MouseRegion(
|
||||
|
|
|
@ -43,13 +43,7 @@ class WorkspacesMenu extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
||||
final ValueNotifier<bool> isShowingMoreActions = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isShowingMoreActions.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -59,7 +53,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
|||
children: [
|
||||
// user email
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
@ -71,18 +65,21 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
|||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
const _WorkspaceMoreButton(),
|
||||
WorkspaceMoreButton(
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0),
|
||||
child: Divider(height: 1.0),
|
||||
),
|
||||
// workspace list
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -93,7 +90,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
|||
userProfile: widget.userProfile,
|
||||
isSelected: workspace.workspaceId ==
|
||||
widget.currentWorkspace.workspaceId,
|
||||
isShowingMoreActions: isShowingMoreActions,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
const VSpace(6.0),
|
||||
],
|
||||
|
@ -102,13 +99,19 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
|||
),
|
||||
),
|
||||
// add new workspace
|
||||
const _CreateWorkspaceButton(),
|
||||
const VSpace(6.0),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: _CreateWorkspaceButton(),
|
||||
),
|
||||
|
||||
if (UniversalPlatform.isDesktop) ...[
|
||||
const _ImportNotionButton(),
|
||||
const VSpace(6.0),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0),
|
||||
child: _ImportNotionButton(),
|
||||
),
|
||||
],
|
||||
|
||||
const VSpace(6.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -132,13 +135,13 @@ class WorkspaceMenuItem extends StatefulWidget {
|
|||
required this.workspace,
|
||||
required this.userProfile,
|
||||
required this.isSelected,
|
||||
required this.isShowingMoreActions,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final UserWorkspacePB workspace;
|
||||
final bool isSelected;
|
||||
final ValueNotifier<bool> isShowingMoreActions;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
State<WorkspaceMenuItem> createState() => _WorkspaceMenuItemState();
|
||||
|
@ -230,7 +233,7 @@ class _WorkspaceMenuItemState extends State<WorkspaceMenuItem> {
|
|||
},
|
||||
child: WorkspaceMoreActionList(
|
||||
workspace: widget.workspace,
|
||||
isShowingMoreActions: widget.isShowingMoreActions,
|
||||
popoverMutex: widget.popoverMutex,
|
||||
),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
|
@ -394,40 +397,35 @@ class _ImportNotionButton extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
FlowyButton(
|
||||
key: importNotionButtonKey,
|
||||
onTap: () {
|
||||
_showImportNotinoDialog(context);
|
||||
child: FlowyButton(
|
||||
key: importNotionButtonKey,
|
||||
onTap: () {
|
||||
_showImportNotinoDialog(context);
|
||||
},
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
text: Row(
|
||||
children: [
|
||||
_buildLeftIcon(context),
|
||||
const HSpace(8.0),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.workspace_importFromNotion.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
rightIcon: FlowyTooltip(
|
||||
message: LocaleKeys.workspace_learnMore.tr(),
|
||||
preferBelow: true,
|
||||
child: FlowyIconButton(
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.information_s,
|
||||
),
|
||||
onPressed: () {
|
||||
afLaunchUrlString(
|
||||
'https://docs.appflowy.io/docs/guides/import-from-notion',
|
||||
);
|
||||
},
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
text: Row(
|
||||
children: [
|
||||
_buildLeftIcon(context),
|
||||
const HSpace(8.0),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.workspace_importFromNotion.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FlowyTooltip(
|
||||
message: LocaleKeys.workspace_learnMore.tr(),
|
||||
preferBelow: true,
|
||||
child: FlowyIconButton(
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.information_s,
|
||||
),
|
||||
onPressed: () {
|
||||
afLaunchUrlString(
|
||||
'https://docs.appflowy.io/docs/guides/import-from-notion',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -478,14 +476,22 @@ class _ImportNotionButton extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _WorkspaceMoreButton extends StatelessWidget {
|
||||
const _WorkspaceMoreButton();
|
||||
@visibleForTesting
|
||||
class WorkspaceMoreButton extends StatelessWidget {
|
||||
const WorkspaceMoreButton({
|
||||
super.key,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
offset: const Offset(0, 6),
|
||||
mutex: popoverMutex,
|
||||
asBarrier: true,
|
||||
popupBuilder: (_) => FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0),
|
||||
leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s),
|
||||
|
|
|
@ -207,6 +207,7 @@ class _SidebarSwitchWorkspaceButtonState
|
|||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 5),
|
||||
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
|
||||
margin: EdgeInsets.zero,
|
||||
animationDuration: Durations.short3,
|
||||
beginScaleFactor: 1.0,
|
||||
beginOpacity: 0.8,
|
||||
|
|
|
@ -641,12 +641,13 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||
popupBuilder: (context) {
|
||||
isIconPickerOpened = true;
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
initialType: iconData.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
ViewBackendService.updateViewIcon(
|
||||
viewId: widget.view.id,
|
||||
viewIcon: result,
|
||||
viewIcon: r.data,
|
||||
);
|
||||
controller.close();
|
||||
if (!r.keepOpen) controller.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -770,13 +771,12 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||
context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());
|
||||
break;
|
||||
case ViewMoreActionType.changeIcon:
|
||||
if (data is! EmojiIconData) {
|
||||
if (data is! SelectedEmojiIconResult) {
|
||||
return;
|
||||
}
|
||||
final result = data;
|
||||
await ViewBackendService.updateViewIcon(
|
||||
viewId: widget.view.id,
|
||||
viewIcon: result,
|
||||
viewIcon: data.data,
|
||||
);
|
||||
break;
|
||||
case ViewMoreActionType.moveTo:
|
||||
|
|
|
@ -58,7 +58,11 @@ class ViewMoreActionPopover extends StatelessWidget {
|
|||
(e) => ViewMoreActionTypeWrapper(e, view, (controller, data) {
|
||||
onEditing(false);
|
||||
onAction(e, data);
|
||||
controller.close();
|
||||
bool enableClose = true;
|
||||
if (data is SelectedEmojiIconResult) {
|
||||
if (data.keepOpen) enableClose = false;
|
||||
}
|
||||
if (enableClose) controller.close();
|
||||
}),
|
||||
)
|
||||
.toList();
|
||||
|
@ -172,6 +176,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
|||
margin: const EdgeInsets.all(0),
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
popupBuilder: (_) => FlowyIconEmojiPicker(
|
||||
initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(),
|
||||
onSelectedEmoji: (result) => onTap(controller, result),
|
||||
),
|
||||
child: child,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
|
@ -51,13 +53,9 @@ class PublishInfoViewItem extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
final icon = publishInfoView.view.icon.value;
|
||||
final icon = publishInfoView.view.icon.toEmojiIconData();
|
||||
return icon.isNotEmpty
|
||||
? FlowyText.emoji(
|
||||
icon,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 18.0,
|
||||
)
|
||||
? RawEmojiIconWidget(emoji: icon, emojiSize: 16.0)
|
||||
: publishInfoView.view.defaultIcon();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
SelectionMenuItem emojiMenuItem = SelectionMenuItem(
|
||||
getName: LocaleKeys.document_plugins_emoji.tr,
|
||||
|
@ -109,7 +108,7 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyEmojiPicker(
|
||||
onEmojiSelected: (_, emoji) => widget.onSubmitted(emoji),
|
||||
onEmojiSelected: (r) => widget.onSubmitted(r.emoji),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/tasks/app_widget.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
|
@ -364,71 +365,59 @@ class OkCancelButton extends StatelessWidget {
|
|||
|
||||
void showToastNotification(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
String? message,
|
||||
TextSpan? richMessage,
|
||||
String? description,
|
||||
ToastificationType type = ToastificationType.success,
|
||||
ToastificationCallbacks? callbacks,
|
||||
double bottomPadding = 100,
|
||||
}) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
toastification.showCustom(
|
||||
alignment: Alignment.bottomCenter,
|
||||
autoCloseDuration: const Duration(milliseconds: 3000),
|
||||
callbacks: callbacks ?? const ToastificationCallbacks(),
|
||||
builder: (_, __) => _MToast(
|
||||
message: message,
|
||||
type: type,
|
||||
bottomPadding: bottomPadding,
|
||||
description: description,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
toastification.show(
|
||||
context: context,
|
||||
type: type,
|
||||
style: ToastificationStyle.flat,
|
||||
closeButtonShowType: CloseButtonShowType.onHover,
|
||||
assert(
|
||||
(message == null) != (richMessage == null),
|
||||
"Exactly one of message or richMessage must be non-null.",
|
||||
);
|
||||
toastification.showCustom(
|
||||
alignment: Alignment.bottomCenter,
|
||||
autoCloseDuration: const Duration(milliseconds: 3000),
|
||||
showProgressBar: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.4),
|
||||
),
|
||||
title: FlowyText(
|
||||
message,
|
||||
maxLines: 3,
|
||||
),
|
||||
description: description != null
|
||||
? FlowyText.regular(
|
||||
description,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.2,
|
||||
maxLines: 3,
|
||||
)
|
||||
: null,
|
||||
callbacks: callbacks ?? const ToastificationCallbacks(),
|
||||
builder: (_, item) {
|
||||
return UniversalPlatform.isMobile
|
||||
? _MobileToast(
|
||||
message: message,
|
||||
type: type,
|
||||
bottomPadding: bottomPadding,
|
||||
description: description,
|
||||
)
|
||||
: _DesktopToast(
|
||||
message: message,
|
||||
richMessage: richMessage,
|
||||
type: type,
|
||||
onDismiss: () => toastification.dismiss(item),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _MToast extends StatelessWidget {
|
||||
const _MToast({
|
||||
required this.message,
|
||||
class _MobileToast extends StatelessWidget {
|
||||
const _MobileToast({
|
||||
this.message,
|
||||
this.type = ToastificationType.success,
|
||||
this.bottomPadding = 100,
|
||||
this.description,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final String? message;
|
||||
final ToastificationType type;
|
||||
final double bottomPadding;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (message == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final hintText = FlowyText.regular(
|
||||
message,
|
||||
message!,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 18.0,
|
||||
color: Colors.white,
|
||||
|
@ -498,6 +487,90 @@ class _MToast extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _DesktopToast extends StatelessWidget {
|
||||
const _DesktopToast({
|
||||
this.message,
|
||||
this.richMessage,
|
||||
required this.type,
|
||||
this.onDismiss,
|
||||
});
|
||||
|
||||
final String? message;
|
||||
final TextSpan? richMessage;
|
||||
final ToastificationType type;
|
||||
final void Function()? onDismiss;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 360.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
margin: const EdgeInsets.only(bottom: 32.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).isLightMode
|
||||
? const Color(0xFF333333)
|
||||
: const Color(0xFF363D49),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// icon
|
||||
FlowySvg(
|
||||
switch (type) {
|
||||
ToastificationType.warning => FlowySvgs.toast_warning_filled_s,
|
||||
ToastificationType.success => FlowySvgs.toast_checked_filled_s,
|
||||
ToastificationType.error => FlowySvgs.toast_error_filled_s,
|
||||
_ => throw UnimplementedError(),
|
||||
},
|
||||
size: const Size.square(20.0),
|
||||
blendMode: null,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
// text
|
||||
Flexible(
|
||||
child: message != null
|
||||
? FlowyText(
|
||||
message!,
|
||||
maxLines: 2,
|
||||
figmaLineHeight: 20.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: const Color(0xFFFFFFFF),
|
||||
)
|
||||
: RichText(
|
||||
text: richMessage!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const HSpace(16.0),
|
||||
// close
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onDismiss,
|
||||
child: const SizedBox.square(
|
||||
dimension: 24.0,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
size: Size.square(16.0),
|
||||
color: Color(0xFFBDBDBD),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showConfirmDeletionDialog({
|
||||
required BuildContext context,
|
||||
required String name,
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
/// This value is used to disable the auto scroll when dragging.
|
||||
///
|
||||
/// It is used to prevent the auto scroll when dragging a view item to a document.
|
||||
bool disableAutoScrollWhenDragging = false;
|
||||
|
||||
class DraggableItem<T extends Object> extends StatefulWidget {
|
||||
const DraggableItem({
|
||||
super.key,
|
||||
|
@ -67,7 +72,7 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
|
|||
childWhenDragging: widget.childWhenDragging ?? widget.child,
|
||||
child: widget.child,
|
||||
onDragUpdate: (details) {
|
||||
if (widget.enableAutoScroll) {
|
||||
if (widget.enableAutoScroll && !disableAutoScrollWhenDragging) {
|
||||
dragTarget = details.globalPosition & widget.hitTestSize;
|
||||
autoScroller?.startAutoScrollIfNecessary(dragTarget!);
|
||||
}
|
||||
|
@ -88,7 +93,7 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
|
|||
}
|
||||
|
||||
void initAutoScrollerIfNeeded(BuildContext context) {
|
||||
if (!widget.enableAutoScroll) {
|
||||
if (!widget.enableAutoScroll || disableAutoScrollWhenDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -104,7 +109,7 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
|
|||
autoScroller = EdgeDraggingAutoScroller(
|
||||
scrollable!,
|
||||
onScrollViewScrolled: () {
|
||||
if (dragTarget != null) {
|
||||
if (dragTarget != null && !disableAutoScrollWhenDragging) {
|
||||
autoScroller!.startAutoScrollIfNecessary(dragTarget!);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -91,13 +91,15 @@ class _RenameViewPopoverState extends State<RenameViewPopover> {
|
|||
}
|
||||
|
||||
Future<void> _updateViewIcon(
|
||||
EmojiIconData emoji,
|
||||
SelectedEmojiIconResult r,
|
||||
PopoverController? _,
|
||||
) async {
|
||||
await ViewBackendService.updateViewIcon(
|
||||
viewId: widget.viewId,
|
||||
viewIcon: emoji,
|
||||
viewIcon: r.data,
|
||||
);
|
||||
widget.popoverController.close();
|
||||
if (!r.keepOpen) {
|
||||
widget.popoverController.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,24 +136,24 @@ SPEC CHECKSUMS:
|
|||
connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
|
||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6
|
||||
hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c
|
||||
irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478
|
||||
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
|
||||
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
|
||||
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3
|
||||
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
|
||||
PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823
|
||||
|
|
|
@ -29,10 +29,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: any_date
|
||||
sha256: "3981efcc15edd1673bcfc1aec298cc6079029fbffb3734c7eae8ceeb878f911e"
|
||||
sha256: e9ed245ba44ccebf3c2d6daa3592213f409821128593d448b219a1f8e9bd17a1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.1.1"
|
||||
app_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -61,8 +61,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: c68e5f6
|
||||
resolved-ref: c68e5f6c585205083e27e875b822656425b2853f
|
||||
ref: "4bcbfb0"
|
||||
resolved-ref: "4bcbfb0679d07d9d4010869ea4bc2f2b7e32c479"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "4.0.0"
|
||||
|
@ -70,8 +70,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/appflowy_editor_plugins"
|
||||
ref: "8047c21"
|
||||
resolved-ref: "8047c21868273d544684522eb61e4ac2d2041409"
|
||||
ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb
|
||||
resolved-ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb
|
||||
url: "https://github.com/AppFlowy-IO/AppFlowy-plugins.git"
|
||||
source: git
|
||||
version: "0.0.6"
|
||||
|
@ -101,10 +101,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
version: "2.6.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -269,10 +269,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
|
||||
sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.2"
|
||||
version: "8.9.3"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -374,26 +374,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5
|
||||
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.2"
|
||||
version: "1.11.1"
|
||||
cross_cache:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_cache
|
||||
sha256: ed30348320a7fefe4195c26cfcbabc76b7108ce3d364c4dd7c1b1c681a4cfe28
|
||||
sha256: "3879d1661f211e89d81ece419684df5111b5a611aa6200cd405e8332031765e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.2"
|
||||
version: "0.0.3"
|
||||
cross_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -406,18 +406,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.6"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.0.2"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -462,10 +462,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba"
|
||||
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "7.0.2"
|
||||
diff_match_patch:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -550,10 +550,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.0.7"
|
||||
expandable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -566,18 +566,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_text_field
|
||||
sha256: "954c7eea1e82728a742f7ddf09b9a51cef087d4f52b716ba88cb3eb78ccd7c6e"
|
||||
sha256: fb5c35460a54906a0ada2a88a968cdfc71d71aebbaf9022debb5d67f47748964
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
version: "15.0.1"
|
||||
extended_text_library:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_text_library
|
||||
sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829"
|
||||
sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.0"
|
||||
version: "12.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -606,26 +606,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12"
|
||||
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.2"
|
||||
version: "8.1.7"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.2+1"
|
||||
version: "0.9.3+2"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
version: "0.9.4+2"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -638,34 +638,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69"
|
||||
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
version: "0.9.3+3"
|
||||
fixnum:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
flex_color_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flex_color_picker
|
||||
sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e"
|
||||
sha256: "12dc855ae8ef5491f529b1fc52c655f06dcdf4114f1f7fdecafa41eec2ec8d79"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.1"
|
||||
version: "3.6.0"
|
||||
flex_seed_scheme:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flex_seed_scheme
|
||||
sha256: "7d97ba5c20f0e5cb1e3e2c17c865e1f797d129de31fc1f75d2dcce9470d6373c"
|
||||
sha256: "7639d2c86268eff84a909026eb169f008064af0fb3696a651b24b0fa24a40334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "3.4.1"
|
||||
flowy_infra:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -703,10 +703,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_animate
|
||||
sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5"
|
||||
sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.0"
|
||||
version: "4.5.2"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -757,8 +757,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "8a9fa49"
|
||||
resolved-ref: "8a9fa491cb3b86baf78b0a33c2c37a29d1cae028"
|
||||
ref: "355aa56"
|
||||
resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8"
|
||||
url: "https://github.com/LucasXu0/emoji_mart.git"
|
||||
source: git
|
||||
version: "1.0.2"
|
||||
|
@ -819,18 +819,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: flutter_shaders
|
||||
sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe"
|
||||
sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
version: "0.1.3"
|
||||
flutter_slidable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_slidable
|
||||
sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3"
|
||||
sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
flutter_staggered_grid_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -851,10 +851,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
|
||||
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.10+1"
|
||||
version: "2.0.16"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -877,10 +877,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc"
|
||||
sha256: "24467dc20bbe49fd63e57d8e190798c4d22cbbdac30e54209d153a15273721d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.8"
|
||||
version: "8.2.10"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -930,10 +930,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
|
||||
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.7"
|
||||
version: "14.6.2"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -994,10 +994,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
|
||||
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.4"
|
||||
version: "0.15.5"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1010,10 +1010,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1058,18 +1058,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50"
|
||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.6"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447"
|
||||
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12"
|
||||
version: "0.8.12+1"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1119,10 +1119,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
irondash_engine_context:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1255,18 +1255,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: logger
|
||||
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
|
||||
sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.5.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1367,10 +1367,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: numerus
|
||||
sha256: "49cd96fe774dd1f574fc9117ed67e8a2b06a612f723e87ef3119456a7729d837"
|
||||
sha256: a17a3f34527497e89378696a76f382b40dc534c4a57b3778de246ebc1ce2ca99
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.3.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1383,34 +1383,34 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: open_filex
|
||||
sha256: ba425ea49affd0a98a234aa9344b9ea5d4c4f7625a1377961eae9fe194c3d523
|
||||
sha256: dcb7bd3d32db8db5260253a62f1564c02c2c8df64bc0187cd213f65f827519bd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.0"
|
||||
version: "4.6.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918
|
||||
sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.2"
|
||||
version: "8.1.2"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66
|
||||
sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1431,18 +1431,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1455,10 +1455,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1519,10 +1519,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
|
||||
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.12"
|
||||
version: "12.0.13"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1535,10 +1535,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
|
||||
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+2"
|
||||
version: "0.1.3+5"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1623,18 +1623,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.5"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
|
||||
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1735,26 +1735,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52"
|
||||
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.2"
|
||||
version: "10.1.3"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5"
|
||||
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "5.0.2"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
|
||||
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.3.3"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1767,10 +1767,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
|
||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.2"
|
||||
version: "2.5.4"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1885,10 +1885,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
|
||||
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.4"
|
||||
version: "1.3.5"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1901,10 +1901,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.12"
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1957,10 +1957,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2029,10 +2029,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: table_calendar
|
||||
sha256: "4ca32b2fc919452c9974abd4c6ea611a63e33b9e4f0b8c38dba3ac1f4a6549d1"
|
||||
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2069,26 +2069,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: time
|
||||
sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221
|
||||
sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.5"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.0.2"
|
||||
toastification:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toastification
|
||||
sha256: "441adf261f03b82db7067cba349756f70e9e2c0b7276bcba856210742f85f394"
|
||||
sha256: "4d97fbfa463dfe83691044cba9f37cb185a79bb9205cfecb655fa1f6be126a13"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.3.0"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2141,10 +2141,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2157,26 +2157,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -2197,10 +2197,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
|
||||
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.3"
|
||||
url_protocol:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -2214,10 +2214,10 @@ packages:
|
|||
dependency: "direct overridden"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.0"
|
||||
version: "4.5.1"
|
||||
value_layout_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2230,26 +2230,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
|
||||
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.11+1"
|
||||
version: "1.1.15"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
|
||||
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.11+1"
|
||||
version: "1.1.12"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
|
||||
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.11+1"
|
||||
version: "1.1.16"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2278,18 +2278,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
|
||||
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2366,10 +2366,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6"
|
||||
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
version: "1.1.5"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -2382,10 +2382,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2398,10 +2398,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
|
|
@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an
|
|||
your data. The best open source alternative to Notion.
|
||||
publish_to: "none"
|
||||
|
||||
version: 0.7.9
|
||||
version: 0.8.0
|
||||
|
||||
environment:
|
||||
flutter: ">=3.22.0"
|
||||
|
@ -73,7 +73,7 @@ dependencies:
|
|||
flutter_emoji_mart:
|
||||
git:
|
||||
url: https://github.com/LucasXu0/emoji_mart.git
|
||||
ref: "8a9fa49"
|
||||
ref: "355aa56"
|
||||
flutter_math_fork: ^0.7.3
|
||||
flutter_slidable: ^3.0.0
|
||||
|
||||
|
@ -174,13 +174,13 @@ dependency_overrides:
|
|||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "c68e5f6"
|
||||
ref: "4bcbfb0"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git
|
||||
path: "packages/appflowy_editor_plugins"
|
||||
ref: "8047c21"
|
||||
ref: "ca8289099e40e0d6ad0605fbbe01fde3091538bb"
|
||||
|
||||
sheet:
|
||||
git:
|
||||
|
|
|
@ -109,7 +109,8 @@ void main() {
|
|||
|
||||
assert(boardBloc.groupControllers.values.length == 2);
|
||||
assert(
|
||||
boardBloc.boardController.groupDatas.last.headerData.groupName == "2024",
|
||||
boardBloc.boardController.groupDatas.last.headerData.groupName ==
|
||||
DateTime.now().year.toString(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -290,5 +290,107 @@ void main() {
|
|||
await editorState.apply(transaction);
|
||||
await completer.future;
|
||||
});
|
||||
|
||||
test('text retain with attributes that are false', () async {
|
||||
final node = paragraphNode(
|
||||
delta: Delta()
|
||||
..insert(
|
||||
'Hello AppFlowy',
|
||||
attributes: {
|
||||
'bold': true,
|
||||
},
|
||||
),
|
||||
);
|
||||
final document = Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
node,
|
||||
],
|
||||
),
|
||||
);
|
||||
final transactionAdapter = TransactionAdapter(
|
||||
documentId: '',
|
||||
documentService: DocumentService(),
|
||||
);
|
||||
|
||||
final editorState = EditorState(
|
||||
document: document,
|
||||
);
|
||||
|
||||
int counter = 0;
|
||||
final completer = Completer();
|
||||
editorState.transactionStream.listen((event) {
|
||||
final time = event.$1;
|
||||
if (time == TransactionTime.before) {
|
||||
final actions = transactionAdapter.transactionToBlockActions(
|
||||
event.$2,
|
||||
editorState,
|
||||
);
|
||||
final textActions =
|
||||
transactionAdapter.filterTextDeltaActions(actions);
|
||||
final blockActions = transactionAdapter.filterBlockActions(actions);
|
||||
expect(textActions.length, 1);
|
||||
expect(blockActions.length, 1);
|
||||
if (counter == 1) {
|
||||
// check text operation
|
||||
final textAction = textActions.first;
|
||||
final textId = textAction.textDeltaPayloadPB?.textId;
|
||||
{
|
||||
expect(textAction.textDeltaType, TextDeltaType.create);
|
||||
|
||||
expect(textId, isNotEmpty);
|
||||
final delta = textAction.textDeltaPayloadPB?.delta;
|
||||
expect(
|
||||
delta,
|
||||
equals(
|
||||
'[{"insert":"Hello","attributes":{"bold":null}},{"insert":" AppFlowy","attributes":{"bold":true}}]',
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (counter == 3) {
|
||||
final textAction = textActions.first;
|
||||
final textId = textAction.textDeltaPayloadPB?.textId;
|
||||
{
|
||||
expect(textAction.textDeltaType, TextDeltaType.update);
|
||||
|
||||
expect(textId, isNotEmpty);
|
||||
final delta = textAction.textDeltaPayloadPB?.delta;
|
||||
expect(
|
||||
delta,
|
||||
equals(
|
||||
'[{"retain":5,"attributes":{"bold":null}}]',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (time == TransactionTime.after && counter == 3) {
|
||||
completer.complete();
|
||||
}
|
||||
});
|
||||
|
||||
counter = 1;
|
||||
final insertTransaction = editorState.transaction;
|
||||
insertTransaction.formatText(node, 0, 5, {
|
||||
'bold': false,
|
||||
});
|
||||
|
||||
await editorState.apply(insertTransaction);
|
||||
|
||||
counter = 2;
|
||||
final updateTransaction = editorState.transaction;
|
||||
updateTransaction.formatText(node, 0, 5, {
|
||||
'bold': true,
|
||||
});
|
||||
await editorState.apply(updateTransaction);
|
||||
|
||||
counter = 3;
|
||||
final formatTransaction = editorState.transaction;
|
||||
formatTransaction.formatText(node, 0, 5, {
|
||||
'bold': false,
|
||||
});
|
||||
await editorState.apply(formatTransaction);
|
||||
|
||||
await completer.future;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -170,5 +170,67 @@ void main() {
|
|||
'0': TableAlign.center.key,
|
||||
});
|
||||
});
|
||||
|
||||
test('delete a column with text color & bold style (1)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 4,
|
||||
);
|
||||
// delete the column 1
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
await editorState.deleteColumnInTable(tableNode, 0);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'0': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'0': true,
|
||||
});
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(tableNode.columnLength, 3);
|
||||
});
|
||||
|
||||
test('delete a column with text color & bold style (2)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 4,
|
||||
);
|
||||
// delete the column 1
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
await editorState.deleteColumnInTable(tableNode, 1);
|
||||
expect(tableNode.columnTextColors, {});
|
||||
expect(tableNode.columnBoldAttributes, {});
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(tableNode.columnLength, 3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -161,5 +161,69 @@ void main() {
|
|||
'1': TableAlign.center.key,
|
||||
});
|
||||
});
|
||||
|
||||
test('duplicate a column with text color & bold style (1)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 4,
|
||||
);
|
||||
// duplicate the column 1
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
await editorState.duplicateColumnInTable(tableNode, 1);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
'2': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
'2': true,
|
||||
});
|
||||
});
|
||||
|
||||
test('duplicate a column with text color & bold style (2)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 4,
|
||||
);
|
||||
// duplicate the column 1
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
await editorState.duplicateColumnInTable(tableNode, 0);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'2': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'2': true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -190,5 +190,67 @@ void main() {
|
|||
'0': TableAlign.center.key,
|
||||
});
|
||||
});
|
||||
|
||||
test('insert a column with text color & bold style (1)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 2,
|
||||
columnCount: 3,
|
||||
);
|
||||
// insert the column at the first position
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'0': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'0': true,
|
||||
});
|
||||
await editorState.insertColumnInTable(tableNode, 0);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
});
|
||||
|
||||
test('insert a column with text color & bold style (2)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 2,
|
||||
columnCount: 3,
|
||||
);
|
||||
// insert the column at the first position
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'0': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'0': true,
|
||||
});
|
||||
await editorState.insertColumnInTable(tableNode, 1);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'0': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'0': true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'simple_table_test_helper.dart';
|
||||
|
@ -193,5 +194,45 @@ void main() {
|
|||
expect(tableNode.tableAlign, align);
|
||||
}
|
||||
});
|
||||
|
||||
test('clear the existing align of the column before updating', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 2,
|
||||
columnCount: 3,
|
||||
);
|
||||
|
||||
final firstCellNode = tableNode.getTableCellNode(
|
||||
rowIndex: 0,
|
||||
columnIndex: 0,
|
||||
);
|
||||
|
||||
Node firstParagraphNode = firstCellNode!.children.first;
|
||||
|
||||
// format the first paragraph to center align
|
||||
final transaction = editorState.transaction;
|
||||
transaction.updateNode(
|
||||
firstParagraphNode,
|
||||
{
|
||||
blockComponentAlign: TableAlign.right.key,
|
||||
},
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
|
||||
firstParagraphNode = editorState.getNodeAtPath([0, 0, 0, 0])!;
|
||||
expect(
|
||||
firstParagraphNode.attributes[blockComponentAlign],
|
||||
TableAlign.right.key,
|
||||
);
|
||||
|
||||
await editorState.updateColumnAlign(
|
||||
tableCellNode: firstCellNode,
|
||||
align: TableAlign.center,
|
||||
);
|
||||
|
||||
expect(
|
||||
firstParagraphNode.attributes[blockComponentAlign],
|
||||
null,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,16 +46,17 @@ void main() {
|
|||
});
|
||||
|
||||
test('putIcons', () async {
|
||||
List<Icon> icons = await RecentIcons.getIcons();
|
||||
List<RecentIcon> icons = await RecentIcons.getIcons();
|
||||
assert(icons.isEmpty);
|
||||
await loadIconGroups();
|
||||
final groups = kIconGroups!;
|
||||
final List<Icon> localIcons = [];
|
||||
final List<RecentIcon> localIcons = [];
|
||||
for (final e in groups) {
|
||||
localIcons.addAll(e.icons);
|
||||
localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());
|
||||
}
|
||||
|
||||
bool equalIcon(Icon a, Icon b) =>
|
||||
bool equalIcon(RecentIcon a, RecentIcon b) =>
|
||||
a.groupName == b.groupName &&
|
||||
a.name == b.name &&
|
||||
a.keywords.equals(b.keywords) &&
|
||||
a.content == b.content;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6 13.8L8.45 11.65C8.26667 11.4667 8.03333 11.375 7.75 11.375C7.46667 11.375 7.23333 11.4667 7.05 11.65C6.86667 11.8333 6.775 12.0667 6.775 12.35C6.775 12.6333 6.86667 12.8667 7.05 13.05L9.9 15.9C10.1 16.1 10.3333 16.2 10.6 16.2C10.8667 16.2 11.1 16.1 11.3 15.9L16.95 10.25C17.1333 10.0667 17.225 9.83333 17.225 9.55C17.225 9.26667 17.1333 9.03333 16.95 8.85C16.7667 8.66667 16.5333 8.575 16.25 8.575C15.9667 8.575 15.7333 8.66667 15.55 8.85L10.6 13.8ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#3AC25C"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 724 B After Width: | Height: | Size: 724 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 17C12.2833 17 12.5208 16.9042 12.7125 16.7125C12.9042 16.5208 13 16.2833 13 16C13 15.7167 12.9042 15.4792 12.7125 15.2875C12.5208 15.0958 12.2833 15 12 15C11.7167 15 11.4792 15.0958 11.2875 15.2875C11.0958 15.4792 11 15.7167 11 16C11 16.2833 11.0958 16.5208 11.2875 16.7125C11.4792 16.9042 11.7167 17 12 17ZM12 13C12.2833 13 12.5208 12.9042 12.7125 12.7125C12.9042 12.5208 13 12.2833 13 12V8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8V12C11 12.2833 11.0958 12.5208 11.2875 12.7125C11.4792 12.9042 11.7167 13 12 13ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#FB006D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.72503 21C2.5417 21 2.37503 20.9542 2.22503 20.8625C2.07503 20.7708 1.95837 20.65 1.87503 20.5C1.7917 20.35 1.74587 20.1875 1.73753 20.0125C1.7292 19.8375 1.77503 19.6667 1.87503 19.5L11.125 3.5C11.225 3.33333 11.3542 3.20833 11.5125 3.125C11.6709 3.04167 11.8334 3 12 3C12.1667 3 12.3292 3.04167 12.4875 3.125C12.6459 3.20833 12.775 3.33333 12.875 3.5L22.125 19.5C22.225 19.6667 22.2709 19.8375 22.2625 20.0125C22.2542 20.1875 22.2084 20.35 22.125 20.5C22.0417 20.65 21.925 20.7708 21.775 20.8625C21.625 20.9542 21.4584 21 21.275 21H2.72503ZM12 18C12.2834 18 12.5209 17.9042 12.7125 17.7125C12.9042 17.5208 13 17.2833 13 17C13 16.7167 12.9042 16.4792 12.7125 16.2875C12.5209 16.0958 12.2834 16 12 16C11.7167 16 11.4792 16.0958 11.2875 16.2875C11.0959 16.4792 11 16.7167 11 17C11 17.2833 11.0959 17.5208 11.2875 17.7125C11.4792 17.9042 11.7167 18 12 18ZM12 15C12.2834 15 12.5209 14.9042 12.7125 14.7125C12.9042 14.5208 13 14.2833 13 14V11C13 10.7167 12.9042 10.4792 12.7125 10.2875C12.5209 10.0958 12.2834 10 12 10C11.7167 10 11.4792 10.0958 11.2875 10.2875C11.0959 10.4792 11 10.7167 11 11V14C11 14.2833 11.0959 14.5208 11.2875 14.7125C11.4792 14.9042 11.7167 15 12 15Z" fill="#FF7E1E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -1,5 +0,0 @@
|
|||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.3327 8.12533C15.3327 12.1754 12.0494 15.4587 7.99935 15.4587C3.94926 15.4587 0.666016 12.1754 0.666016 8.12533C0.666016 4.07524 3.94926 0.791992 7.99935 0.791992C12.0494 0.791992 15.3327 4.07524 15.3327 8.12533Z" fill="#FB006D"/>
|
||||
<path d="M8.00065 4.79199C7.63246 4.79199 7.33398 5.09047 7.33398 5.45866V8.79199C7.33398 9.16018 7.63246 9.45866 8.00065 9.45866C8.36884 9.45866 8.66732 9.16018 8.66732 8.79199V5.45866C8.66732 5.09047 8.36884 4.79199 8.00065 4.79199Z" fill="white"/>
|
||||
<path d="M8.00065 10.1253C7.63246 10.1253 7.33398 10.4238 7.33398 10.792C7.33398 11.1602 7.63246 11.4587 8.00065 11.4587C8.36884 11.4587 8.66732 11.1602 8.66732 10.792C8.66732 10.4238 8.36884 10.1253 8.00065 10.1253Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 830 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue