Compare commits

..
Author SHA1 Message Date
Nathan 89647ae683 chore: logs for windows 2025-03-31 00:15:59 +08:00
520 changed files with 6006 additions and 26193 deletions

No files matched your search

+1 -1
View File
@@ -338,7 +338,7 @@ jobs:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-22.04,
os: ubuntu-20.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
-31
View File
@@ -1,35 +1,4 @@
# Release Notes
## Version 0.8.9 - 16/04/2025
### Desktop
#### New Features
- Supported pasting a link as a mention, providing a more condensed visualization of linked content
- Supported converting between link formats (e.g. transforming a mention into a bookmark)
- Improved the link editing experience with enhanced UX
- Added OTP (One-Time Password) support for sign-in authentication
- Added latest AI models: GPT-4.1, GPT-4.1-mini, and Claude 3.7 Sonnet
#### Bug Fixes
- Fixed an issue where properties were not displaying in the row detail page
- Fixed a bug where Undo didn't work in the row detail page
- Fixed an issue where blocks didn't grow when the grid got bigger
- Fixed several bugs related to AI writers
### Mobile
#### New Features
- Added sign-in with OTP (One-Time Password)
#### Bug Fixes
- Fixed an issue where the slash menu sometimes failed to display
- Updated the mention page block to handle page selection with more context.
## Version 0.8.8 - 01/04/2025
### New Features
- Added support for selecting AI models in AI writer
- Revamped link menu in toolbar
- Added support for using ":" to add emojis in documents
- Passed the history of past AI prompts and responses to AI writer
### Bug Fixes
- Improved AI writer scrolling user experience
- Fixed issue where checklist items would disappear during reordering
- Fixed numbered lists generated by AI to maintain the same index as the input
## Version 0.8.7 - 18/03/2025
### New Features
- Made local AI free and integrated with Ollama
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.8.9"
APPFLOWY_VERSION = "0.8.8"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -4,7 +4,6 @@ analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "packages/**/*.dart"
linter:
rules:
Whitespace-only changes.
File diff suppressed because it is too large. Load diff
@@ -15,6 +15,7 @@ void main() {
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapContinousAnotherWay();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
@@ -6,7 +6,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -44,12 +44,12 @@ void main() {
await tester.pumpAndSettle(const Duration(milliseconds: 200));
// Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna)
expect(find.byType(SearchResultCell), findsNWidgets(2));
expect(find.byType(SearchResultTile), findsNWidgets(2));
// The score should be higher for "ViewOna" thus it should be shown first
final secondDocumentWidget = tester
.widget(find.byType(SearchResultCell).first) as SearchResultCell;
expect(secondDocumentWidget.item.displayName, secondDocument);
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
expect(secondDocumentWidget.result.data, secondDocument);
// Change search to "ViewOne"
await tester.enterText(searchFieldFinder, firstDocument);
@@ -57,9 +57,9 @@ void main() {
// The score should be higher for "ViewOne" thus it should be shown first
final firstDocumentWidget = tester.widget(
find.byType(SearchResultCell).first,
) as SearchResultCell;
expect(firstDocumentWidget.item.displayName, firstDocument);
find.byType(SearchResultTile).first,
) as SearchResultTile;
expect(firstDocumentWidget.result.data, firstDocument);
});
testWidgets('Displaying icons in search results', (tester) async {
@@ -89,11 +89,11 @@ void main() {
);
await tester.enterText(searchFieldFinder, 'Page-$randomValue');
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.byType(SearchResultCell), findsNWidgets(2));
expect(find.byType(SearchResultTile), findsNWidgets(2));
/// check results
final svgs = find.descendant(
of: find.byType(SearchResultCell),
of: find.byType(SearchResultTile),
matching: find.byType(FlowySvg),
);
expect(svgs, findsNWidgets(2));
@@ -1,5 +1,5 @@
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -27,12 +27,11 @@ void main() {
expect(find.byType(RecentViewsList), findsOneWidget);
// Expect three recent history items
expect(find.byType(SearchRecentViewCell), findsNWidgets(3));
expect(find.byType(RecentViewTile), findsNWidgets(3));
// Expect the first item to be the last viewed document
final firstDocumentWidget =
tester.widget(find.byType(SearchRecentViewCell).first)
as SearchRecentViewCell;
tester.widget(find.byType(RecentViewTile).first) as RecentViewTile;
expect(firstDocumentWidget.view.name, secondDocument);
});
});
@@ -15,7 +15,6 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a database and add a linked database view
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
@@ -30,11 +29,6 @@ void main() {
await tester.tapHidePropertyButton();
tester.noFieldWithName('New field 1');
// create another field, New field 1 to be hidden still
await tester.tapNewPropertyButton();
await tester.dismissFieldEditor();
tester.noFieldWithName('New field 1');
// go back to inline database view, expect field to be shown
await tester.tapTabBarLinkedViewByViewName('Untitled');
tester.findFieldWithName('New field 1');
@@ -66,40 +60,5 @@ void main() {
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1");
});
testWidgets('field cell width', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a database and add a linked database view
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// check the width of the field
expect(tester.getFieldWidth('New field 1'), 150);
// change the width of the field
await tester.changeFieldWidth('New field 1', 200);
expect(tester.getFieldWidth('New field 1'), 205);
// create another field, New field 1 to be same width
await tester.tapNewPropertyButton();
await tester.dismissFieldEditor();
expect(tester.getFieldWidth('New field 1'), 205);
// go back to inline database view, expect New field 1 to be 150px
await tester.tapTabBarLinkedViewByViewName('Untitled');
expect(tester.getFieldWidth('New field 1'), 150);
// go back to linked database view, expect New field 1 to be 205px
await tester.tapTabBarLinkedViewByViewName('Grid');
expect(tester.getFieldWidth('New field 1'), 205);
});
});
}
@@ -1,10 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -78,37 +73,5 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('insert grid in column', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// create page and show slash menu
await tester.createNewPageWithNameUnderParent(name: 'test page');
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
/// create a column
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_twoColumns.tr(),
);
final actionList = find.byType(BlockActionList);
expect(actionList, findsNWidgets(2));
final position = tester.getCenter(actionList.last);
/// tap the second child of column
await tester.tapAt(position.copyWith(dx: position.dx + 50));
/// create a grid
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_grid.tr(),
);
final grid = find.byType(GridPageContent);
expect(grid, findsOneWidget);
});
});
}
@@ -1,12 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
@@ -322,14 +320,8 @@ void main() {
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) async {
final pasteAsMenu = find.byType(PasteAsMenu);
expect(pasteAsMenu, findsOneWidget);
final bookmarkButton = find.text(
LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(),
);
await tester.tapButton(bookmarkButton);
// the second one is the paragraph node
expect(editorState.document.root.children.length, 1);
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
@@ -341,20 +333,19 @@ void main() {
await tester.hoverOnWidget(
find.byType(CustomLinkPreviewWidget),
onHover: () async {
/// show menu
final menu = find.byType(CustomLinkPreviewMenu);
expect(menu, findsOneWidget);
await tester.tapButton(menu);
final convertToLinkButton = find.text(
LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl
.tr(),
);
final convertToLinkButton = find.byWidgetPredicate((widget) {
return widget is MenuBlockButton &&
widget.tooltip ==
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
});
expect(convertToLinkButton, findsOneWidget);
await tester.tapButton(convertToLinkButton);
await tester.tap(convertToLinkButton);
await tester.pumpAndSettle();
},
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final textNode = editorState.getNodeAtPath([0])!;
expect(textNode.type, ParagraphBlockKeys.type);
@@ -372,19 +363,14 @@ void main() {
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) async {
final pasteAsMenu = find.byType(PasteAsMenu);
expect(pasteAsMenu, findsOneWidget);
final bookmarkButton = find.text(
LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(),
);
await tester.tapButton(bookmarkButton);
// the second one is the paragraph node
expect(editorState.document.root.children.length, 1);
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
await tester.editor.tapLineOfEditorAt(0);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed:
@@ -483,6 +469,16 @@ void main() {
});
});
testWidgets('paste image url without extension', (tester) async {
const plainText =
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
await tester.pasteContent(plainText: plainText, (editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});
const testMarkdownText = '''
# I'm h1
## I'm h2
@@ -525,7 +521,7 @@ void main() {
extension on WidgetTester {
Future<void> pasteContent(
FutureOr<void> Function(EditorState editorState) test, {
void Function(EditorState editorState) test, {
Future<void> Function(EditorState editorState)? beforeTest,
String? plainText,
String? html,
@@ -562,6 +558,6 @@ extension on WidgetTester {
);
await pumpAndSettle(const Duration(milliseconds: 1000));
await test(editor.getCurrentEditorState());
test(editor.getCurrentEditorState());
}
}
@@ -13,8 +13,6 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final finder = find.text(gettingStarted, findRichText: true);
await tester.pumpUntilFound(finder, timeout: const Duration(seconds: 2));
// create a new document
const pageName = 'Test Document';
@@ -1,453 +0,0 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const avaliableLink = 'https://appflowy.io/',
unavailableLink = 'www.thereIsNoting.com';
Future<void> preparePage(WidgetTester tester, {String? pageName}) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: pageName);
await tester.editor.tapLineOfEditorAt(0);
}
Future<void> pasteLink(WidgetTester tester, String link) async {
await getIt<ClipboardService>()
.setData(ClipboardServiceData(plainText: link));
/// paste the link
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle(Duration(seconds: 1));
}
Future<void> pasteAs(
WidgetTester tester,
String link,
PasteMenuType type, {
Duration waitTime = const Duration(milliseconds: 500),
}) async {
await pasteLink(tester, link);
final convertToMentionButton = find.text(type.title);
await tester.tapButton(convertToMentionButton);
await tester.pumpAndSettle(waitTime);
}
void checkUrl(Node node, String link) {
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{
'insert': link,
'attributes': {'href': link},
}
]);
}
void checkMention(Node node, String link) {
final delta = node.delta!;
final insert = (delta.first as TextInsert).text;
final attributes = delta.first.attributes;
expect(insert, MentionBlockKeys.mentionChar);
final mention =
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
expect(mention[MentionBlockKeys.type], MentionType.externalLink.name);
expect(mention[MentionBlockKeys.url], avaliableLink);
}
void checkBookmark(Node node, String link) {
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], link);
}
void checkEmbed(Node node, String link) {
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkEmbedKeys.previewType], LinkEmbedKeys.embed);
expect(node.attributes[LinkPreviewBlockKeys.url], link);
}
group('Paste as URL', () {
Future<void> pasteAndTurnInto(
WidgetTester tester,
String link,
String title,
) async {
await pasteLink(tester, link);
final convertToLinkButton = find
.text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());
await tester.tapButton(convertToLinkButton);
/// hover link and turn into mention
await tester.hoverOnWidget(
find.byType(LinkHoverTrigger),
onHover: () async {
final turnintoButton = find.byFlowySvg(FlowySvgs.turninto_m);
await tester.tapButton(turnintoButton);
final convertToButton = find.text(title);
await tester.tapButton(convertToButton);
await tester.pumpAndSettle(Duration(seconds: 1));
},
);
}
testWidgets('paste a link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteLink(tester, link);
final convertToLinkButton = find
.text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());
await tester.tapButton(convertToLinkButton);
final node = tester.editor.getNodeAtPath([0]);
checkUrl(node, link);
});
testWidgets('paste a link and turn into mention', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAndTurnInto(
tester,
link,
LinkConvertMenuCommand.toMention.title,
);
/// check metion values
final node = tester.editor.getNodeAtPath([0]);
checkMention(node, link);
});
testWidgets('paste a link and turn into bookmark', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAndTurnInto(
tester,
link,
LinkConvertMenuCommand.toBookmark.title,
);
/// check metion values
final node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, link);
});
testWidgets('paste a link and turn into embed', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAndTurnInto(
tester,
link,
LinkConvertMenuCommand.toEmbed.title,
);
/// check metion values
final node = tester.editor.getNodeAtPath([0]);
checkEmbed(node, link);
});
});
group('Paste as Mention', () {
Future<void> pasteAsMention(WidgetTester tester, String link) =>
pasteAs(tester, link, PasteMenuType.mention);
String getMentionLink(Node node) {
final insert = node.delta?.first as TextInsert?;
final mention = insert?.attributes?[MentionBlockKeys.mention]
as Map<String, dynamic>?;
return mention?[MentionBlockKeys.url] ?? '';
}
Future<void> hoverMentionAndClick(
WidgetTester tester,
String command,
) async {
final mentionLink = find.byType(MentionLinkBlock);
expect(mentionLink, findsOneWidget);
await tester.hoverOnWidget(
mentionLink,
onHover: () async {
final errorPreview = find.byType(MentionLinkErrorPreview);
expect(errorPreview, findsOneWidget);
final convertButton = find.byFlowySvg(FlowySvgs.turninto_m);
await tester.tapButton(convertButton);
final menuButton = find.text(command);
await tester.tapButton(menuButton);
},
);
}
testWidgets('paste a link as mention', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
final node = tester.editor.getNodeAtPath([0]);
checkMention(node, link);
});
testWidgets('paste as mention and copy link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
final mentionLink = find.byType(MentionLinkBlock);
expect(mentionLink, findsOneWidget);
await tester.hoverOnWidget(
mentionLink,
onHover: () async {
final preview = find.byType(MentionLinkPreview);
if (!preview.hasFound) {
final copyButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
await tester.tapButton(copyButton);
} else {
final moreOptionButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);
await tester.tapButton(moreOptionButton);
final copyButton =
find.text(MentionLinktMenuCommand.copyLink.title);
await tester.tapButton(copyButton);
}
},
);
final clipboardContent = await getIt<ClipboardService>().getData();
expect(clipboardContent.plainText, link);
});
testWidgets('paste as error mention and turninto url', (tester) async {
String link = unavailableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
Node node = tester.editor.getNodeAtPath([0]);
link = getMentionLink(node);
await hoverMentionAndClick(
tester,
MentionLinktErrorMenuCommand.toURL.title,
);
node = tester.editor.getNodeAtPath([0]);
checkUrl(node, link);
});
testWidgets('paste as error mention and turninto embed', (tester) async {
String link = unavailableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
Node node = tester.editor.getNodeAtPath([0]);
link = getMentionLink(node);
await hoverMentionAndClick(
tester,
MentionLinktErrorMenuCommand.toEmbed.title,
);
node = tester.editor.getNodeAtPath([0]);
checkEmbed(node, link);
});
testWidgets('paste as error mention and turninto bookmark', (tester) async {
String link = unavailableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
Node node = tester.editor.getNodeAtPath([0]);
link = getMentionLink(node);
await hoverMentionAndClick(
tester,
MentionLinktErrorMenuCommand.toBookmark.title,
);
node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, link);
});
testWidgets('paste as error mention and remove link', (tester) async {
String link = unavailableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
Node node = tester.editor.getNodeAtPath([0]);
link = getMentionLink(node);
await hoverMentionAndClick(
tester,
MentionLinktErrorMenuCommand.removeLink.title,
);
node = tester.editor.getNodeAtPath([0]);
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{'insert': link},
]);
});
});
group('Paste as Bookmark', () {
Future<void> pasteAsBookmark(WidgetTester tester, String link) =>
pasteAs(tester, link, PasteMenuType.bookmark);
Future<void> hoverAndClick(
WidgetTester tester,
LinkPreviewMenuCommand command,
) async {
final bookmark = find.byType(CustomLinkPreviewBlockComponent);
expect(bookmark, findsOneWidget);
await tester.hoverOnWidget(
bookmark,
onHover: () async {
final menuButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);
await tester.tapButton(menuButton);
final commandButton = find.text(command.title);
await tester.tapButton(commandButton);
},
);
}
testWidgets('paste a link as bookmark', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
final node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, link);
});
testWidgets('paste a link as bookmark and convert to mention',
(tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToMention);
final node = tester.editor.getNodeAtPath([0]);
checkMention(node, link);
});
testWidgets('paste a link as bookmark and convert to url', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToUrl);
final node = tester.editor.getNodeAtPath([0]);
checkUrl(node, link);
});
testWidgets('paste a link as bookmark and convert to embed',
(tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToEmbed);
final node = tester.editor.getNodeAtPath([0]);
checkEmbed(node, link);
});
testWidgets('paste a link as bookmark and copy link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.copyLink);
final clipboardContent = await getIt<ClipboardService>().getData();
expect(clipboardContent.plainText, link);
});
testWidgets('paste a link as bookmark and replace link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.replace);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
await tester.enterText(find.byType(TextFormField), unavailableLink);
await tester.tapButton(find.text(LocaleKeys.button_replace.tr()));
final node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, unavailableLink);
});
testWidgets('paste a link as bookmark and remove link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.removeLink);
final node = tester.editor.getNodeAtPath([0]);
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{'insert': link},
]);
});
});
group('Paste as Embed', () {
Future<void> pasteAsEmbed(WidgetTester tester, String link) =>
pasteAs(tester, link, PasteMenuType.embed);
Future<void> hoverAndConvert(
WidgetTester tester,
LinkEmbedConvertCommand command,
) async {
final embed = find.byType(LinkEmbedBlockComponent);
expect(embed, findsOneWidget);
await tester.hoverOnWidget(
embed,
onHover: () async {
final menuButton = find.byFlowySvg(FlowySvgs.turninto_m);
await tester.tapButton(menuButton);
final commandButton = find.text(command.title);
await tester.tapButton(commandButton);
},
);
}
testWidgets('paste a link as embed', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsEmbed(tester, link);
final node = tester.editor.getNodeAtPath([0]);
checkEmbed(node, link);
});
testWidgets('paste a link as bookmark and convert to mention',
(tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsEmbed(tester, link);
await hoverAndConvert(tester, LinkEmbedConvertCommand.toMention);
final node = tester.editor.getNodeAtPath([0]);
checkMention(node, link);
});
testWidgets('paste a link as bookmark and convert to url', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsEmbed(tester, link);
await hoverAndConvert(tester, LinkEmbedConvertCommand.toURL);
final node = tester.editor.getNodeAtPath([0]);
checkUrl(node, link);
});
testWidgets('paste a link as bookmark and convert to bookmark',
(tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsEmbed(tester, link);
await hoverAndConvert(tester, LinkEmbedConvertCommand.toBookmark);
final node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, link);
});
});
}
@@ -1,6 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -48,41 +47,5 @@ void main() {
expect(editorState.selection!.start.offset, 0);
});
testWidgets('select and delete text', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// create a new document
await tester.createNewPageWithNameUnderParent();
/// input text
final editor = tester.editor;
final editorState = editor.getCurrentEditorState();
const inputText = 'Test for text selection and deletion';
final texts = inputText.split(' ');
await editor.tapLineOfEditorAt(0);
await tester.ime.insertText(inputText);
/// selecte and delete
int index = 0;
while (texts.isNotEmpty) {
final text = texts.removeAt(0);
await tester.editor.updateSelection(
Selection(
start: Position(path: [0], offset: index),
end: Position(path: [0], offset: index + text.length),
),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
index++;
}
/// excpete the text value is correct
final node = editorState.getNodeAtPath([0])!;
final nodeText = node.delta?.toPlainText() ?? '';
expect(nodeText, ' ' * (index - 1));
});
});
}
@@ -13,7 +13,6 @@ import 'document_with_multi_image_block_test.dart'
as document_with_multi_image_block_test;
import 'document_with_simple_table_test.dart'
as document_with_simple_table_test;
import 'document_link_preview_test.dart' as document_link_preview_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -29,5 +28,4 @@ void main() {
document_find_menu_test.main();
document_toolbar_test.main();
document_with_simple_table_test.main();
document_link_preview_test.main();
}
@@ -184,45 +184,6 @@ void main() {
3,
);
});
testWidgets('toolbar will not rebuild after click item', (tester) async {
const text = 'Test rebuilding';
await prepareForToolbar(tester, text);
Finder toolbar = find.byType(DesktopFloatingToolbar);
Element toolbarElement = toolbar.evaluate().first;
final elementHashcode = toolbarElement.hashCode;
final boldButton = find.byFlowySvg(FlowySvgs.toolbar_bold_m),
underlineButton = find.byFlowySvg(FlowySvgs.toolbar_underline_m),
italicButton = find.byFlowySvg(FlowySvgs.toolbar_inline_italic_m);
/// tap format buttons
await tester.tapButton(boldButton);
await tester.tapButton(underlineButton);
await tester.tapButton(italicButton);
toolbar = find.byType(DesktopFloatingToolbar);
toolbarElement = toolbar.evaluate().first;
/// check if the toolbar is not rebuilt
expect(elementHashcode, toolbarElement.hashCode);
final editorState = tester.editor.getCurrentEditorState();
/// check text formats
expect(
editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.bold),
true,
);
expect(
editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.italic),
true,
);
expect(
editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.underline),
true,
);
});
});
group('document toolbar: link', () {
@@ -267,7 +228,6 @@ void main() {
expect(toolbar, findsNothing);
/// show toolbar again
await tester.editor.tapLineOfEditorAt(0);
await selectText(tester, text);
await tester.tapButton(linkButton);
@@ -43,15 +43,12 @@ void main() {
});
group('insert emoji by colon', () {
Future<void> createNewDocumentAndShowEmojiList(
WidgetTester tester, {
String? search,
}) async {
Future<void> createNewDocumentAndShowEmojiList(WidgetTester tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(':${search ?? 'a'}');
await tester.ime.insertText(':');
await tester.pumpAndSettle(Duration(seconds: 1));
}
@@ -109,10 +106,11 @@ void main() {
});
testWidgets('insert with searching', (tester) async {
await createNewDocumentAndShowEmojiList(tester, search: 's');
await createNewDocumentAndShowEmojiList(tester);
/// search for `smiling eyes`, IME is not working, use keyboard input
final searchText = [
LogicalKeyboardKey.keyS,
LogicalKeyboardKey.keyM,
LogicalKeyboardKey.keyI,
LogicalKeyboardKey.keyL,
@@ -140,10 +138,16 @@ void main() {
});
testWidgets('start searching with sapce', (tester) async {
await createNewDocumentAndShowEmojiList(tester, search: ' ');
await createNewDocumentAndShowEmojiList(tester);
/// emoji list is showing
final emojiHandler = find.byType(EmojiHandler);
expect(emojiHandler, findsOneWidget);
/// input space
await tester.simulateKeyEvent(LogicalKeyboardKey.space);
/// emoji list is dismissed
expect(emojiHandler, findsNothing);
});
});
@@ -67,10 +67,12 @@ extension CommonOperations on WidgetTester {
} else {
// cloud version
final anonymousButton = find.byType(SignInAnonymousButtonV2);
await tapButton(anonymousButton, warnIfMissed: true);
await tapButton(anonymousButton);
}
await pumpAndSettle(const Duration(milliseconds: 200));
if (Platform.isWindows) {
await pumpAndSettle(const Duration(milliseconds: 200));
}
}
Future<void> tapContinousAnotherWay() async {
@@ -942,31 +942,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 200));
}
Future<void> changeFieldWidth(String fieldName, double width) async {
final field = find.byWidgetPredicate(
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
);
await hoverOnWidget(
field,
onHover: () async {
final dragHandle = find.descendant(
of: field,
matching: find.byType(DragToExpandLine),
);
await drag(dragHandle, Offset(width - getSize(field).width, 0));
await pumpAndSettle(const Duration(milliseconds: 200));
},
);
}
double getFieldWidth(String fieldName) {
final field = find.byWidgetPredicate(
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
);
return getSize(field).width;
}
Future<void> findDateEditor(dynamic matcher) async {
final finder = find.byType(DateCellEditor);
expect(finder, matcher);
@@ -307,11 +307,9 @@ class EditorOperations {
Future<void> openTurnIntoMenu(Path path) async {
await hoverAndClickOptionMenuButton(path);
await tester.tapButton(
find
.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
)
.first,
find.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
),
);
await tester.pumpUntilFound(find.byType(TurnIntoOptionMenu));
}
+23 -23
View File
@@ -181,37 +181,37 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
@@ -23,7 +23,6 @@ class DesktopPromptInput extends StatefulWidget {
required this.selectedSourcesNotifier,
required this.onUpdateSelectedSources,
this.hideDecoration = false,
this.hideFormats = false,
this.extraBottomActionButton,
});
@@ -35,7 +34,6 @@ class DesktopPromptInput extends StatefulWidget {
final ValueNotifier<List<String>> selectedSourcesNotifier;
final void Function(List<String>) onUpdateSelectedSources;
final bool hideDecoration;
final bool hideFormats;
final Widget? extraBottomActionButton;
@override
@@ -141,11 +139,11 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
children: [
ConstrainedBox(
constraints: getTextFieldConstraints(
state.showPredefinedFormats && !widget.hideFormats,
state.showPredefinedFormats,
),
child: inputTextField(),
),
if (state.showPredefinedFormats && !widget.hideFormats)
if (state.showPredefinedFormats)
Positioned.fill(
bottom: null,
child: TextFieldTapRegion(
@@ -170,9 +168,8 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
top: null,
child: TextFieldTapRegion(
child: _PromptBottomActions(
showPredefinedFormatBar:
showPredefinedFormats:
state.showPredefinedFormats,
showPredefinedFormatButton: !widget.hideFormats,
onTogglePredefinedFormatSection: () =>
context.read<AIPromptInputBloc>().add(
AIPromptInputEvent
@@ -574,8 +571,7 @@ class PromptInputTextField extends StatelessWidget {
class _PromptBottomActions extends StatelessWidget {
const _PromptBottomActions({
required this.sendButtonState,
required this.showPredefinedFormatBar,
required this.showPredefinedFormatButton,
required this.showPredefinedFormats,
required this.onTogglePredefinedFormatSection,
required this.onStartMention,
required this.onSendPressed,
@@ -585,8 +581,7 @@ class _PromptBottomActions extends StatelessWidget {
this.extraBottomActionButton,
});
final bool showPredefinedFormatBar;
final bool showPredefinedFormatButton;
final bool showPredefinedFormats;
final void Function() onTogglePredefinedFormatSection;
final void Function() onStartMention;
final SendButtonState sendButtonState;
@@ -605,12 +600,10 @@ class _PromptBottomActions extends StatelessWidget {
builder: (context, state) {
return Row(
children: [
if (showPredefinedFormatButton) ...[
_predefinedFormatButton(),
const HSpace(
DesktopAIChatSizes.inputActionBarButtonSpacing,
),
],
_predefinedFormatButton(),
const HSpace(
DesktopAIChatSizes.inputActionBarButtonSpacing,
),
SelectModelMenu(
aiModelStateNotifier:
context.read<AIPromptInputBloc>().aiModelStateNotifier,
@@ -648,7 +641,7 @@ class _PromptBottomActions extends StatelessWidget {
Widget _predefinedFormatButton() {
return PromptInputDesktopToggleFormatButton(
showFormatBar: showPredefinedFormatBar,
showFormatBar: showPredefinedFormats,
onTap: onTogglePredefinedFormatSection,
);
}
@@ -173,6 +173,7 @@ class _ModelItem extends StatelessWidget {
model.i18n,
figmaLineHeight: 20,
overflow: TextOverflow.ellipsis,
color: isSelected ? Theme.of(context).colorScheme.primary : null,
),
if (model.desc.isNotEmpty)
FlowyText(
@@ -45,18 +45,16 @@ Future<bool> afLaunchUri(
}
// try to launch the uri directly
bool result = await launcher.canLaunchUrl(uri);
if (result) {
try {
result = await launcher.launchUrl(
uri,
mode: mode,
webOnlyWindowName: webOnlyWindowName,
);
} on PlatformException catch (e) {
Log.error('Failed to open uri: $e');
return false;
}
bool result;
try {
result = await launcher.launchUrl(
uri,
mode: mode,
webOnlyWindowName: webOnlyWindowName,
);
} on PlatformException catch (e) {
Log.error('Failed to open uri: $e');
return false;
}
// if the uri is not a valid url, try to launch it with http scheme
@@ -135,6 +133,7 @@ Future<bool> _afLaunchLocalUri(
};
if (context != null && context.mounted) {
showToastNotification(
context,
message: message,
type: result.type == ResultType.done
? ToastificationType.success
@@ -16,8 +16,6 @@ const double _kMinimumWidth = 112.0;
const double _kDefaultHorizontalPadding = 12.0;
typedef CompareFunction<T> = bool Function(T? left, T? right);
// Navigation shortcuts to move the selected menu items up or down.
final Map<ShortcutActivator, Intent> _kMenuTraversalShortcuts =
<ShortcutActivator, Intent>{
@@ -88,7 +86,6 @@ class AFDropdownMenu<T> extends StatefulWidget {
this.requestFocusOnTap,
this.expandedInsets,
this.searchCallback,
this.selectOptionCompare,
required this.dropdownMenuEntries,
});
@@ -270,11 +267,6 @@ class AFDropdownMenu<T> extends StatefulWidget {
/// which contains the contents of the text input field.
final SearchCallback<T>? searchCallback;
/// Defines the compare function for the menu items.
///
/// Defaults to null. If this is null, the menu items will be sorted by the label.
final CompareFunction<T>? selectOptionCompare;
@override
State<AFDropdownMenu<T>> createState() => _AFDropdownMenuState<T>();
}
@@ -309,16 +301,7 @@ class _AFDropdownMenuState<T> extends State<AFDropdownMenu<T>> {
filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
final int index = filteredEntries.indexWhere(
(DropdownMenuEntry<T> entry) {
if (widget.selectOptionCompare != null) {
return widget.selectOptionCompare!(
entry.value,
widget.initialSelection,
);
} else {
return entry.value == widget.initialSelection;
}
},
(DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection,
);
if (index != -1) {
_textEditingController.value = TextEditingValue(
@@ -336,6 +336,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
listener: (context, state) {
if (state.isLocked) {
showToastNotification(
context,
message: LocaleKeys.lockPage_pageLockedToast.tr(),
);
@@ -365,6 +366,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
listener: (context, state) {
if (state.isLocked) {
showToastNotification(
context,
message: LocaleKeys.lockPage_pageLockedToast.tr(),
);
}
@@ -66,7 +66,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
break;
case MobileViewBottomSheetBodyAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
Navigator.of(context).pop();
context.pop();
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
_addFavorite(context);
@@ -161,6 +161,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
context.pop();
showToastNotification(
context,
message: LocaleKeys.button_duplicateSuccessfully.tr(),
);
}
@@ -169,6 +170,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
_toggleFavorite(context);
showToastNotification(
context,
message: LocaleKeys.button_favoriteSuccessfully.tr(),
);
}
@@ -177,6 +179,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
_toggleFavorite(context);
showToastNotification(
context,
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
);
}
@@ -199,7 +202,8 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
),
);
showToastNotification(
message: LocaleKeys.message_copy_success.tr(),
context,
message: LocaleKeys.grid_url_copy.tr(),
);
}
}
@@ -230,10 +234,12 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
),
);
showToastNotification(
context,
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
);
} else {
showToastNotification(
context,
message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(),
type: ToastificationType.error,
);
@@ -317,9 +323,11 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
if (state.publishResult != null) {
state.publishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_publishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
type: ToastificationType.error,
),
@@ -327,9 +335,11 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
} else if (state.unpublishResult != null) {
state.unpublishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishFailed.tr(),
description: error.msg,
type: ToastificationType.error,
@@ -339,6 +349,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
state.updatePathNameResult!.onSuccess(
(value) {
showToastNotification(
context,
message:
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
);
@@ -65,6 +65,7 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
Navigator.pop(context);
context.read<ViewBloc>().add(const ViewEvent.duplicate());
showToastNotification(
context,
message: LocaleKeys.button_duplicateSuccessfully.tr(),
);
break;
@@ -83,6 +84,7 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(widget.view));
showToastNotification(
context,
message: !widget.view.isFavorite
? LocaleKeys.button_favoriteSuccessfully.tr()
: LocaleKeys.button_unfavoriteSuccessfully.tr(),
@@ -144,6 +146,7 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
Navigator.pop(context);
showToastNotification(
context,
message: LocaleKeys.sideBar_removeSuccess.tr(),
);
},
@@ -182,7 +182,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
_divider(),
..._buildPublishActions(context),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
@@ -236,7 +236,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
MobileViewBottomSheetBodyAction.unpublish,
),
),
_divider(),
];
} else {
return [
@@ -247,7 +246,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
MobileViewBottomSheetBodyAction.publish,
),
),
_divider(),
];
}
}
@@ -45,6 +45,7 @@ enum MobilePaneActionType {
size: 24.0,
onPressed: (context) {
showToastNotification(
context,
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
);
@@ -60,6 +61,7 @@ enum MobilePaneActionType {
size: 24.0,
onPressed: (context) {
showToastNotification(
context,
message: LocaleKeys.button_favoriteSuccessfully.tr(),
);
@@ -103,7 +103,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
Log.info('Open row page(${widget.documentId})');
if (view == null) {
showToastNotification(message: 'Failed to open row page');
showToastNotification(context, message: 'Failed to open row page');
// reload the view again
unawaited(_preloadView(context));
Log.error('Failed to open row page(${widget.documentId})');
@@ -329,7 +329,7 @@ class _HomePageState extends State<_HomePage> {
}
if (message != null) {
showToastNotification(message: message, type: toastType);
showToastNotification(context, message: message, type: toastType);
}
}
}
@@ -71,6 +71,12 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
}
Widget _buildSettingsWidget(UserProfilePB userProfile) {
// show the third-party sign in buttons if user logged in with local session and auth is enabled.
final isLocalAuthEnabled =
userProfile.authenticator == AuthenticatorPB.Local && isAuthEnabled;
'';
return BlocProvider(
create: (context) => UserWorkspaceBloc(userProfile: userProfile)
..add(const UserWorkspaceEvent.initial()),
@@ -99,7 +105,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
const AboutSettingGroup(),
UserSessionSettingGroup(
userProfile: userProfile,
showThirdPartyLogin: false,
showThirdPartyLogin: isLocalAuthEnabled,
),
const VSpace(20),
],
@@ -16,7 +16,6 @@ enum _MobileSettingsPopupMenuItem {
members,
trash,
help,
helpAndDocumentation,
}
class HomePageSettingsPopupMenu extends StatelessWidget {
@@ -63,16 +62,10 @@ class HomePageSettingsPopupMenu extends StatelessWidget {
text: LocaleKeys.settings_popupMenuItem_trash.tr(),
),
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _MobileSettingsPopupMenuItem.helpAndDocumentation,
svg: FlowySvgs.help_and_documentation_s,
text: LocaleKeys.settings_popupMenuItem_helpAndDocumentation.tr(),
),
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _MobileSettingsPopupMenuItem.help,
svg: FlowySvgs.message_support_s,
text: LocaleKeys.settings_popupMenuItem_getSupport.tr(),
text: LocaleKeys.settings_popupMenuItem_helpAndSupport.tr(),
),
],
onSelected: (_MobileSettingsPopupMenuItem value) {
@@ -89,9 +82,6 @@ class HomePageSettingsPopupMenu extends StatelessWidget {
case _MobileSettingsPopupMenuItem.help:
_openHelpPage(context);
break;
case _MobileSettingsPopupMenuItem.helpAndDocumentation:
_openHelpAndDocumentationPage(context);
break;
}
},
child: const Padding(
@@ -133,10 +123,6 @@ class HomePageSettingsPopupMenu extends StatelessWidget {
void _openSettingsPage(BuildContext context) {
context.push(MobileHomeSettingPage.routeName);
}
void _openHelpAndDocumentationPage(BuildContext context) {
afLaunchUrlString('https://appflowy.com/guide');
}
}
class _PopupButton extends StatelessWidget {
@@ -339,6 +339,7 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
context.read<SpaceBloc>().add(const SpaceEvent.duplicate());
showToastNotification(
context,
message: LocaleKeys.space_success_duplicateSpace.tr(),
);
@@ -373,6 +374,7 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
.add(SpaceEvent.rename(space: widget.space, name: name));
showToastNotification(
context,
message: LocaleKeys.space_success_renameSpace.tr(),
);
},
@@ -422,6 +424,7 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
);
showToastNotification(
context,
message: LocaleKeys.space_success_updateSpace.tr(),
);
@@ -454,6 +457,7 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
context.read<SpaceBloc>().add(SpaceEvent.delete(widget.space));
showToastNotification(
context,
message: LocaleKeys.space_success_deleteSpace.tr(),
);
@@ -102,7 +102,7 @@ class MobileInlineActionsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasIcon = item.iconBuilder != null;
final hasIcon = item.icon != null;
return Container(
height: 36,
decoration: BoxDecoration(
@@ -119,7 +119,7 @@ class MobileInlineActionsWidget extends StatelessWidget {
child: Row(
children: [
if (hasIcon) ...[
item.iconBuilder!.call(isSelected),
item.icon!.call(isSelected),
SizedBox(width: 12),
],
Flexible(
@@ -332,6 +332,7 @@ class _NotificationNavigationBar extends StatelessWidget {
}
showToastNotification(
context,
message: LocaleKeys
.settings_notifications_markAsReadNotifications_allSuccess
.tr(),
@@ -349,6 +350,7 @@ class _NotificationNavigationBar extends StatelessWidget {
}
showToastNotification(
context,
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
.tr(),
);
@@ -108,6 +108,7 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
void _onMarkAllAsRead(BuildContext context) {
showToastNotification(
context,
message: LocaleKeys
.settings_notifications_markAsReadNotifications_allSuccess
.tr(),
@@ -118,6 +119,7 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
void _onArchiveAll(BuildContext context) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
.tr(),
);
@@ -131,6 +133,7 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
}
showToastNotification(
context,
message: 'Unarchive all success (Debug Mode)',
);
@@ -31,6 +31,7 @@ enum NotificationPaneActionType {
size: 24.0,
onPressed: (context) {
showToastNotification(
context,
message: LocaleKeys
.settings_notifications_markAsReadNotifications_success
.tr(),
@@ -54,6 +55,7 @@ enum NotificationPaneActionType {
size: 24.0,
onPressed: (context) {
showToastNotification(
context,
message: 'Unarchive notification success',
);
@@ -166,6 +168,7 @@ class _NotificationMoreActions extends StatelessWidget {
Navigator.of(context).pop();
showToastNotification(
context,
message: LocaleKeys.settings_notifications_markAsReadNotifications_success
.tr(),
);
@@ -188,6 +191,7 @@ class _NotificationMoreActions extends StatelessWidget {
void _onArchive(BuildContext context) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_archiveNotifications_success
.tr()
.tr(),
@@ -74,6 +74,7 @@ class _NotificationTabState extends State<NotificationTab>
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_refreshSuccess.tr(),
);
}
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -70,7 +71,6 @@ class MobileSelectionMenu extends SelectionMenuService {
final editorWidth = editorState.renderBox!.size.width;
_positionNotifier = ValueNotifier(position);
final showAtTop = position.top != null;
_selectionMenuEntry = OverlayEntry(
builder: (context) {
return SizedBox(
@@ -94,7 +94,6 @@ class MobileSelectionMenu extends SelectionMenuService {
child: MobileSelectionMenuWidget(
selectionMenuStyle: style,
singleColumn: singleColumn,
showAtTop: showAtTop,
items: selectionMenuItems
..forEach((element) {
if (element is MobileSelectionMenuItem) {
@@ -167,8 +166,7 @@ class MobileSelectionMenu extends SelectionMenuService {
if (selectionRects.isEmpty) {
return null;
}
final screenSize = MediaQuery.of(context).size;
calculateSelectionMenuOffset(selectionRects.first, screenSize);
calculateSelectionMenuOffset(selectionRects.first);
final (left, top, right, bottom) = getPosition();
return _Position(left, top, right, bottom);
}
@@ -207,65 +205,50 @@ class MobileSelectionMenu extends SelectionMenuService {
return (left, top, right, bottom);
}
void calculateSelectionMenuOffset(Rect rect, Size screenSize) {
void calculateSelectionMenuOffset(Rect rect) {
// Workaround: We can customize the padding through the [EditorStyle],
// but the coordinates of overlay are not properly converted currently.
// Just subtract the padding here as a result.
const menuHeight = 192.0, menuWidth = 240.0;
const menuHeight = 192.0, menuWidth = 240.0 + 10;
const menuOffset = Offset(0, 10);
final editorOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final editorHeight = editorState.renderBox!.size.height;
final screenHeight = screenSize.height;
final editorWidth = editorState.renderBox!.size.width;
final rectHeight = rect.height;
// show below default
_alignment = Alignment.bottomRight;
final bottomRight = rect.topLeft;
final offset = bottomRight;
final limitX = editorWidth + editorOffset.dx - menuWidth,
limitY = screenHeight -
editorHeight +
editorOffset.dy -
menuHeight -
rectHeight;
_alignment = Alignment.topLeft;
final bottomRight = rect.bottomRight;
final topRight = rect.topRight;
var offset = bottomRight + menuOffset;
final limitX = editorWidth - menuWidth + editorOffset.dx;
_offset = Offset(
editorWidth - offset.dx - menuWidth,
screenHeight - offset.dy - menuHeight - rectHeight,
min(offset.dx, limitX),
offset.dy,
);
// show above
if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {
/// show above
if (offset.dy > menuHeight) {
_offset = Offset(
_offset.dx,
offset.dy - menuHeight,
);
_alignment = Alignment.topRight;
} else {
_offset = Offset(
_offset.dx,
limitY,
);
}
offset = topRight - menuOffset;
_alignment = Alignment.bottomLeft;
_offset = Offset(
offset.dx,
editorOffset.dy + editorHeight - offset.dy,
);
}
if (offset.dx + menuWidth >= editorOffset.dx + editorWidth) {
/// show left
if (offset.dx > menuWidth) {
_alignment = _alignment == Alignment.bottomRight
? Alignment.bottomLeft
: Alignment.topLeft;
_offset = Offset(
offset.dx - menuWidth,
_offset.dy,
);
} else {
_offset = Offset(
limitX,
_offset.dy,
);
}
// show on left
if (_offset.dx - editorOffset.dx > editorWidth / 2) {
_alignment = _alignment == Alignment.topLeft
? Alignment.topRight
: Alignment.bottomRight;
final x = editorWidth - _offset.dx + editorOffset.dx;
_offset = Offset(
min(x, limitX),
_offset.dy,
);
}
}
}
@@ -22,7 +22,6 @@ class MobileSelectionMenuWidget extends StatefulWidget {
required this.deleteSlashByDefault,
required this.singleColumn,
required this.startOffset,
required this.showAtTop,
this.nameBuilder,
});
@@ -39,7 +38,6 @@ class MobileSelectionMenuWidget extends StatefulWidget {
final bool deleteSlashByDefault;
final bool singleColumn;
final bool showAtTop;
final int startOffset;
final SelectionMenuItemNameBuilder? nameBuilder;
@@ -174,37 +172,27 @@ class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 192,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.showAtTop) Spacer(),
Focus(
focusNode: _focusNode,
child: DecoratedBox(
decoration: BoxDecoration(
color: widget.selectionMenuStyle.selectionMenuBackgroundColor,
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withValues(alpha: 0.1),
),
],
borderRadius: BorderRadius.circular(6.0),
),
child: _showingItems.isEmpty
? _buildNoResultsWidget(context)
: _buildResultsWidget(
context,
_showingItems,
widget.itemCountFilter,
),
return Focus(
focusNode: _focusNode,
child: DecoratedBox(
decoration: BoxDecoration(
color: widget.selectionMenuStyle.selectionMenuBackgroundColor,
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withValues(alpha: 0.1),
),
),
if (!widget.showAtTop) Spacer(),
],
],
borderRadius: BorderRadius.circular(6.0),
),
child: _showingItems.isEmpty
? _buildNoResultsWidget(context)
: _buildResultsWidget(
context,
_showingItems,
widget.itemCountFilter,
),
),
);
}
@@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget {
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.com/privacy'),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.com/terms'),
onTap: () => afLaunchUrlString('https://appflowy.io/terms'),
),
if (kDebugMode)
MobileSettingItem(
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@@ -5,10 +7,10 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../widgets/widgets.dart';
import 'personal_info.dart';
class PersonalInfoSettingGroup extends StatelessWidget {
@@ -30,7 +32,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
selector: (state) => state.userProfile.name,
builder: (context, userName) {
return MobileSettingGroup(
groupTitle: LocaleKeys.settings_accountPage_title.tr(),
groupTitle: LocaleKeys.settings_mobile_personalInfo.tr(),
settingItemList: [
MobileSettingItem(
name: userName,
@@ -81,6 +81,7 @@ class SupportSettingGroup extends StatelessWidget {
);
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
);
}
@@ -63,15 +63,8 @@ class UserSessionSettingGroup extends StatelessWidget {
);
},
builder: (context, state) {
return Column(
children: [
const ContinueWithEmailAndPassword(),
const VSpace(12.0),
const ThirdPartySignInButtons(
expanded: true,
),
const VSpace(16.0),
],
return const ThirdPartySignInButtons(
expanded: true,
);
},
),
@@ -201,6 +201,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
result.fold(
(s) {
showToastNotification(
context,
message:
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
bottomPadding: keyboardHeight,
@@ -217,6 +218,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
context,
type: ToastificationType.error,
bottomPadding: keyboardHeight,
message: message,
@@ -227,6 +229,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
result.fold(
(s) {
showToastNotification(
context,
message:
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
bottomPadding: keyboardHeight,
@@ -244,6 +247,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
context,
type: ToastificationType.error,
message: message,
bottomPadding: keyboardHeight,
@@ -254,6 +258,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
result.fold(
(s) {
showToastNotification(
context,
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceSuccess
.tr(),
@@ -262,6 +267,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
},
(f) {
showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceFailed
@@ -276,11 +282,11 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
void _inviteMember(BuildContext context) {
final email = emailController.text;
if (!isEmail(email)) {
showToastNotification(
return showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
return;
}
context
.read<WorkspaceMemberBloc>()
@@ -27,7 +27,7 @@ class ChatAIAvatar extends StatelessWidget {
child: const CircleAvatar(
backgroundColor: Colors.transparent,
child: FlowySvg(
FlowySvgs.app_logo_s,
FlowySvgs.flowy_logo_s,
size: Size.square(16),
blendMode: null,
),
@@ -46,7 +46,7 @@ class ChatWelcomePage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(
FlowySvgs.app_logo_xl,
FlowySvgs.flowy_logo_xl,
size: Size.square(32),
blendMode: null,
),
@@ -184,7 +184,8 @@ class CopyButton extends StatelessWidget {
);
if (context.mounted) {
showToastNotification(
message: LocaleKeys.message_copy_success.tr(),
context,
message: LocaleKeys.grid_url_copiedNotification.tr(),
);
}
},
@@ -376,7 +376,8 @@ class ChatAIMessagePopup extends StatelessWidget {
}
if (context.mounted) {
showToastNotification(
message: LocaleKeys.message_copy_success.tr(),
context,
message: LocaleKeys.grid_url_copiedNotification.tr(),
);
}
},
@@ -14,6 +14,7 @@ import 'package:universal_platform/universal_platform.dart';
void openPageFromMessage(BuildContext context, ViewPB? view) {
if (view == null) {
showToastNotification(
context,
message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),
type: ToastificationType.error,
);
@@ -35,6 +36,7 @@ void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
return;
}
showToastNotification(
context,
richMessage: TextSpan(
children: [
TextSpan(
@@ -241,11 +241,6 @@ class SelectOptionCellEditorBloc
} else if (!state.selectedOptions
.any((option) => option.id == focusedOptionId)) {
_selectOptionService.select(optionIds: [focusedOptionId]);
emit(
state.copyWith(
clearFilter: true,
),
);
}
}
@@ -411,28 +411,23 @@ class FieldController {
/// Listen for field setting changes in the backend.
void _listenOnFieldSettingsChanged() {
FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
final newFields = [...fieldInfos];
final List<FieldInfo> newFields = fieldInfos;
var updatedField = newFields.firstOrNull;
if (newFields.isEmpty) {
if (updatedField == null) {
return null;
}
final index = newFields
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
if (index != -1) {
newFields[index] =
newFields[index].copyWith(fieldSettings: updatedFieldSettings);
_fieldNotifier.fieldInfos = newFields;
_fieldSettings
..removeWhere(
(field) => field.fieldId == updatedFieldSettings.fieldId,
)
..add(updatedFieldSettings);
return newFields[index];
updatedField = newFields[index];
}
return null;
_fieldNotifier.fieldInfos = newFields;
return updatedField;
}
_fieldSettingsListener.start(
@@ -73,24 +73,27 @@ class RelatedRowDetailPageBloc
});
}
/// initialize bloc through the `database_id` and `row_id`. The process is as
/// follows:
/// 1. use the `database_id` to get the database meta, which contains the
/// `inline_view_id`
/// 2. use the `inline_view_id` to instantiate a `DatabaseController`.
/// 3. use the `row_id` with the DatabaseController` to create `RowController`
void _init(String databaseId, String initialRowId) async {
final viewId = await DatabaseEventGetDefaultDatabaseViewId(
DatabaseIdPB(value: databaseId),
).send().fold(
(pb) => pb.value,
(error) => null,
);
if (viewId == null) {
final databaseMeta =
await DatabaseEventGetDatabaseMeta(DatabaseIdPB(value: databaseId))
.send()
.fold<DatabaseMetaPB?>((s) => s, (f) => null);
if (databaseMeta == null) {
return;
}
final databaseView = await ViewBackendService.getView(viewId)
.fold((viewPB) => viewPB, (f) => null);
if (databaseView == null) {
final inlineView =
await ViewBackendService.getView(databaseMeta.inlineViewId)
.fold((viewPB) => viewPB, (f) => null);
if (inlineView == null) {
return;
}
final databaseController = DatabaseController(view: databaseView);
final databaseController = DatabaseController(view: inlineView);
await databaseController.open().fold(
(s) => databaseController.setIsLoading(false),
(f) => null,
@@ -101,7 +104,7 @@ class RelatedRowDetailPageBloc
}
final rowController = RowController(
rowMeta: rowInfo.rowMeta,
viewId: databaseView.id,
viewId: inlineView.id,
rowCache: databaseController.rowCache,
);
@@ -386,15 +386,15 @@ class _BoardContentState extends State<_BoardContent> {
scrollManager: scrollManager,
),
),
cardBuilder: (cardContext, column, columnItem) =>
cardBuilder: (context, column, columnItem) =>
MultiBlocProvider(
key: ValueKey("board_card_${column.id}_${columnItem.id}"),
providers: [
BlocProvider<BoardBloc>.value(
value: cardContext.read<BoardBloc>(),
value: context.read<BoardBloc>(),
),
BlocProvider.value(
value: cardContext.read<BoardActionsCubit>(),
value: context.read<BoardActionsCubit>(),
),
BlocProvider(
create: (_) => ViewLockStatusBloc(view: widget.view)
@@ -402,7 +402,7 @@ class _BoardContentState extends State<_BoardContent> {
),
],
child: BlocBuilder<ViewLockStatusBloc, ViewLockStatusState>(
builder: (lockStatusContext, state) {
builder: (context, state) {
return IgnorePointer(
ignoring: state.isLocked,
child: _BoardCard(
@@ -412,13 +412,6 @@ class _BoardContentState extends State<_BoardContent> {
notifier: widget.focusScope,
cellBuilder: cellBuilder,
compactMode: compactMode,
onOpenCard: (rowMeta) => _openCard(
context: context,
databaseController: lockStatusContext
.read<BoardBloc>()
.databaseController,
rowMeta: rowMeta,
),
),
);
},
@@ -588,7 +581,6 @@ class _BoardCard extends StatefulWidget {
required this.cellBuilder,
required this.notifier,
required this.compactMode,
required this.onOpenCard,
});
final AppFlowyGroupData afGroupData;
@@ -597,7 +589,6 @@ class _BoardCard extends StatefulWidget {
final CardCellBuilder cellBuilder;
final BoardFocusScope notifier;
final bool compactMode;
final void Function(RowMetaPB) onOpenCard;
@override
State<_BoardCard> createState() => _BoardCardState();
@@ -707,8 +698,10 @@ class _BoardCardState extends State<_BoardCard> {
groupingFieldId: widget.groupItem.fieldInfo.id,
isEditing: _isEditing,
cellBuilder: widget.cellBuilder,
onTap: (context) => widget.onOpenCard(
context.read<CardBloc>().rowController.rowMeta,
onTap: (context) => _openCard(
context: context,
databaseController: databaseController,
rowMeta: context.read<CardBloc>().rowController.rowMeta,
),
onShiftTap: (_) {
Focus.of(context).requestFocus();
@@ -108,7 +108,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
top: 0,
bottom: 0,
right: 0,
child: DragToExpandLine(),
child: _DragToExpandLine(),
);
return _GridHeaderCellContainer(
@@ -158,11 +158,8 @@ class _GridHeaderCellContainer extends StatelessWidget {
}
}
@visibleForTesting
class DragToExpandLine extends StatelessWidget {
const DragToExpandLine({
super.key,
});
class _DragToExpandLine extends StatelessWidget {
const _DragToExpandLine();
@override
Widget build(BuildContext context) {
@@ -362,13 +362,9 @@ const kDatabasePluginWidgetBuilderActionBuilder = 'action_builder';
const kDatabasePluginWidgetBuilderNode = 'node';
class DatabasePluginWidgetBuilderSize {
const DatabasePluginWidgetBuilderSize({
required this.horizontalPadding,
this.verticalPadding = 16.0,
});
const DatabasePluginWidgetBuilderSize({required this.horizontalPadding});
final double horizontalPadding;
final double verticalPadding;
}
class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';
@@ -14,7 +16,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:universal_platform/universal_platform.dart';
import '../editable_cell_skeleton/checklist.dart';
@@ -200,16 +201,19 @@ class _ChecklistItems extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(),
proxyDecorator: (child, index, _) => Material(
color: Colors.transparent,
child: MouseRegion(
cursor: UniversalPlatform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: IgnorePointer(
child: BlocProvider.value(
child: Stack(
children: [
BlocProvider.value(
value: context.read<ChecklistCellBloc>(),
child: child,
),
),
MouseRegion(
cursor: Platform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: const SizedBox.expand(),
),
],
),
),
buildDefaultDragHandles: false,
@@ -203,7 +203,7 @@ class MobileURLEditor extends StatelessWidget {
ClipboardData(text: textEditingController.text),
);
Fluttertoast.showToast(
msg: LocaleKeys.message_copy_success.tr(),
msg: LocaleKeys.grid_url_copiedNotification.tr(),
gravity: ToastGravity.BOTTOM,
);
context.pop();
@@ -14,7 +14,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../application/cell/bloc/checklist_cell_bloc.dart';
import 'checklist_cell_textfield.dart';
@@ -126,16 +125,19 @@ class ChecklistItemList extends StatelessWidget {
shrinkWrap: true,
proxyDecorator: (child, index, _) => Material(
color: Colors.transparent,
child: MouseRegion(
cursor: UniversalPlatform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: IgnorePointer(
child: BlocProvider.value(
child: Stack(
children: [
BlocProvider.value(
value: context.read<ChecklistCellBloc>(),
child: child,
),
),
MouseRegion(
cursor: Platform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: const SizedBox.expand(),
),
],
),
),
buildDefaultDragHandles: false,
@@ -5,7 +5,6 @@ import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart';
import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
@@ -121,34 +120,29 @@ class _RowEditor extends StatelessWidget {
return context;
},
dispose: (_, editorContext) => editorContext.dispose(),
child: AiWriterScrollWrapper(
child: EditorDropHandler(
viewId: view.id,
editorState: editorState,
child: EditorDropHandler(
isLocalMode: context.read<DocumentBloc>().isLocalMode,
dropManagerState: context.read<EditorDropManagerState>(),
child: EditorTransactionService(
viewId: view.id,
editorState: editorState,
isLocalMode: context.read<DocumentBloc>().isLocalMode,
dropManagerState: context.read<EditorDropManagerState>(),
child: EditorTransactionService(
viewId: view.id,
editorState: editorState,
child: Provider(
create: (context) => DatabasePluginWidgetBuilderSize(
horizontalPadding: 0,
),
child: AppFlowyEditorPage(
shrinkWrap: true,
autoFocus: false,
editorState: editorState,
styleCustomizer: EditorStyleCustomizer(
context: context,
padding: const EdgeInsets.only(left: 16, right: 54),
),
showParagraphPlaceholder: (editorState, _) =>
editorState.document.isEmpty,
placeholderText: (_) =>
LocaleKeys.cardDetails_notesPlaceholder.tr(),
child: Provider(
create: (context) =>
DatabasePluginWidgetBuilderSize(horizontalPadding: 0),
child: AppFlowyEditorPage(
shrinkWrap: true,
autoFocus: false,
editorState: editorState,
styleCustomizer: EditorStyleCustomizer(
context: context,
padding: const EdgeInsets.only(left: 16, right: 54),
),
showParagraphPlaceholder: (editorState, _) =>
editorState.document.isEmpty,
placeholderText: (_) =>
LocaleKeys.cardDetails_notesPlaceholder.tr(),
),
),
),
@@ -1,4 +1,3 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
@@ -8,10 +7,8 @@ import 'package:appflowy/plugins/database/widgets/row/row_property.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
@@ -21,10 +18,8 @@ import 'package:appflowy/workspace/application/action_navigation/navigation_acti
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import '../../workspace/application/view/view_bloc.dart';
@@ -53,6 +48,18 @@ class DatabaseDocumentPage extends StatefulWidget {
class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
EditorState? editorState;
@override
void initState() {
super.initState();
EditorNotification.addListener(_onEditorNotification);
}
@override
void dispose() {
EditorNotification.removeListener(_onEditorNotification);
super.dispose();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@@ -97,11 +104,7 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
return BlocListener<ActionNavigationBloc, ActionNavigationState>(
listener: _onNotificationAction,
listenWhen: (_, curr) => curr.action != null,
child: AiWriterScrollWrapper(
viewId: widget.view.id,
editorState: editorState,
child: _buildEditorPage(context, state),
),
child: _buildEditorPage(context, state),
);
},
),
@@ -118,34 +121,21 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
styleCustomizer: EditorStyleCustomizer(
context: context,
padding: EditorStyleCustomizer.documentPadding,
editorState: state.editorState!,
),
header: _buildDatabaseDataContent(context, state.editorState!),
initialSelection: widget.initialSelection,
useViewInfoBloc: false,
placeholderText: (node) =>
node.type == ParagraphBlockKeys.type && !node.isInTable
? LocaleKeys.editor_slashPlaceHolder.tr()
: '',
),
);
return Provider(
create: (_) {
final context = SharedEditorContext();
context.isInDatabaseRowPage = true;
return context;
},
dispose: (_, editorContext) => editorContext.dispose(),
child: EditorTransactionService(
viewId: widget.view.id,
editorState: state.editorState!,
child: Column(
children: [
if (state.isDeleted) _buildBanner(context),
Expanded(child: appflowyEditorPage),
],
),
return EditorTransactionService(
viewId: widget.view.id,
editorState: state.editorState!,
child: Column(
children: [
if (state.isDeleted) _buildBanner(context),
Expanded(child: appflowyEditorPage),
],
),
);
}
@@ -218,6 +208,20 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
);
}
void _onEditorNotification(EditorNotificationType type) {
final editorState = this.editorState;
if (editorState == null) {
return;
}
if (type == EditorNotificationType.undo) {
undoCommand.execute(editorState);
} else if (type == EditorNotificationType.redo) {
redoCommand.execute(editorState);
} else if (type == EditorNotificationType.exitEditing) {
editorState.selection = null;
}
}
void _onNotificationAction(
BuildContext context,
ActionNavigationState state,
@@ -272,9 +272,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
}
if (options.inMemoryUpdate) {
if (enableDocumentInternalLog) {
Log.trace('skip transaction for in-memory update');
}
Log.trace('skip transaction for in-memory update');
return;
}
@@ -442,6 +440,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
final context = AppGlobals.rootNavKey.currentContext;
if (context != null && context.mounted) {
showToastNotification(
context,
message: 'document integrity check failed',
type: ToastificationType.error,
);
@@ -16,7 +16,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
import 'editor_plugins/link_preview/custom_link_preview_block_component.dart';
import 'editor_plugins/page_block/custom_page_block_component.dart';
/// A global configuration for the editor.
@@ -970,11 +969,11 @@ OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder(
);
}
CustomLinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(
LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(
BuildContext context,
BlockComponentConfiguration configuration,
) {
return CustomLinkPreviewBlockComponentBuilder(
return LinkPreviewBlockComponentBuilder(
configuration: configuration.copyWith(
padding: (node) {
if (UniversalPlatform.isMobile) {
@@ -983,6 +982,21 @@ CustomLinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(
return const EdgeInsets.symmetric(vertical: 10);
},
),
cache: LinkPreviewDataCache(),
showMenu: true,
menuBuilder: (context, node, state) => Positioned(
top: 10,
right: 0,
child: LinkPreviewMenu(node: node, state: state),
),
builder: (_, node, url, title, description, imageUrl) =>
CustomLinkPreviewWidget(
node: node,
url: url,
title: title,
description: description,
imageUrl: imageUrl,
),
);
}
@@ -20,9 +20,9 @@ import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys;
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -352,6 +352,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
final themeV2 = AFThemeExtensionV2.of(context);
final editor = Directionality(
textDirection: textDirection,
child: AppFlowyEditor(
@@ -430,46 +431,45 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
),
);
}
final appTheme = AppFlowyTheme.of(context);
return Center(
child: BlocProvider.value(
value: context.read<DocumentBloc>(),
child: FloatingToolbar(
floatingToolbarHeight: 40,
padding: EdgeInsets.symmetric(horizontal: 6),
style: FloatingToolbarStyle(
backgroundColor: Theme.of(context).cardColor,
toolbarActiveColor: Color(0xffe0f8fd),
),
items: toolbarItems,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(appTheme.borderRadius.l),
color: appTheme.surfaceColorScheme.primary,
boxShadow: [appTheme.shadow.small],
),
toolbarBuilder: (_, child, onDismiss, isMetricsChanged) =>
BlocProvider.value(
value: context.read<DocumentBloc>(),
child: DesktopFloatingToolbar(
editorState: editorState,
onDismiss: onDismiss,
enableAnimation: !isMetricsChanged,
child: child,
),
),
placeHolderBuilder: (_) => customPlaceholderItem,
editorState: editorState,
editorScrollController: editorScrollController,
textDirection: textDirection,
tooltipBuilder: (context, id, message, child) =>
widget.styleCustomizer.buildToolbarItemTooltip(
context,
id,
message,
child,
),
child: editor,
child: FloatingToolbar(
floatingToolbarHeight: 40,
padding: EdgeInsets.symmetric(horizontal: 6),
style: FloatingToolbarStyle(
backgroundColor: Theme.of(context).cardColor,
toolbarActiveColor: Color(0xffe0f8fd),
),
items: toolbarItems,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).cardColor,
boxShadow: [
BoxShadow(
offset: Offset(0, 4),
blurRadius: 24,
color: themeV2.shadow_medium,
),
],
),
toolbarBuilder: (context, child, onDismiss, isMetricsChanged) =>
DesktopFloatingToolbar(
editorState: editorState,
onDismiss: onDismiss,
enableAnimation: !isMetricsChanged,
child: child,
),
placeHolderBuilder: (_) => customPlaceholderItem,
editorState: editorState,
editorScrollController: editorScrollController,
textDirection: textDirection,
tooltipBuilder: (context, id, message, child) =>
widget.styleCustomizer.buildToolbarItemTooltip(
context,
id,
message,
child,
),
child: editor,
),
);
}
@@ -490,12 +490,6 @@ class MainContentArea extends StatelessWidget {
return DesktopPromptInput(
isStreaming: false,
hideDecoration: true,
hideFormats: [
AiWriterCommand.fixSpellingAndGrammar,
AiWriterCommand.improveWriting,
AiWriterCommand.makeLonger,
AiWriterCommand.makeShorter,
].contains(state.command),
textController: textController,
onSubmitted: (message, format, _) {
cubit.runCommand(state.command, message, format);
@@ -5,8 +5,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -119,7 +119,7 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
}
Widget buildChild(BuildContext context) {
final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorTheme;
final themeV2 = AFThemeExtensionV2.of(context);
final child = FlowyIconButton(
width: 48,
height: 32,
@@ -131,13 +131,13 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
FlowySvg(
FlowySvgs.toolbar_ai_writer_m,
size: Size.square(20),
color: iconScheme.primary,
color: themeV2.icon_primary,
),
HSpace(4),
FlowySvg(
FlowySvgs.toolbar_arrow_down_m,
size: Size(12, 20),
color: iconScheme.primary,
color: themeV2.icon_tertiary,
),
],
),
@@ -150,6 +150,7 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
});
} else {
showToastNotification(
context,
message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
);
}
@@ -180,7 +181,6 @@ class ImproveWritingButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context);
final child = FlowyIconButton(
width: 36,
height: 32,
@@ -188,7 +188,7 @@ class ImproveWritingButton extends StatelessWidget {
icon: FlowySvg(
FlowySvgs.toolbar_ai_improve_writing_m,
size: Size.square(20.0),
color: theme.iconColorTheme.primary,
color: AFThemeExtensionV2.of(context).icon_primary,
),
onPressed: () {
if (_isAIEnabled(editorState)) {
@@ -196,6 +196,7 @@ class ImproveWritingButton extends StatelessWidget {
_insertAiNode(editorState, AiWriterCommand.improveWriting);
} else {
showToastNotification(
context,
message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
);
}
@@ -16,11 +16,6 @@ import 'ai_writer_block_operations.dart';
import 'ai_writer_entities.dart';
import 'ai_writer_node_extension.dart';
/// Enable the debug log for the AiWriterCubit.
///
/// This is useful for debugging the AI writer cubit.
const _aiWriterCubitDebugLog = true;
class AiWriterCubit extends Cubit<AiWriterState> {
AiWriterCubit({
required this.documentId,
@@ -57,13 +52,8 @@ class AiWriterCubit extends Cubit<AiWriterState> {
bool withDiscard = true,
bool withUnformat = true,
}) async {
if (aiWriterNode == null) {
return;
}
if (withDiscard) {
await _textRobot.discard(
afterSelection: aiWriterNode!.aiWriterSelection,
);
await _textRobot.discard();
}
_textRobot.clear();
_textRobot.reset();
@@ -106,10 +96,6 @@ class AiWriterCubit extends Cubit<AiWriterState> {
final command = node.aiWriterCommand;
final (run, prompt) = await _addSelectionTextToRecords(command);
_aiWriterCubitLog(
'command: $command, run: $run, prompt: $prompt',
);
if (!run) {
await exit();
return;
@@ -226,32 +212,20 @@ class AiWriterCubit extends Cubit<AiWriterState> {
return;
}
// Accept
//
// If the user clicks accept, we need to replace the selection with the AI's response
if (action case SuggestionAction.accept) {
// trim the markdown text to avoid extra new lines
final trimmedMarkdownText = _textRobot.markdownText.trim();
_aiWriterCubitLog(
'trigger accept action, markdown text: $trimmedMarkdownText',
);
await _textRobot.persist();
await formatSelection(
editorState,
selection,
ApplySuggestionFormatType.clear,
);
await _textRobot.deleteAINodes();
await _textRobot.replace(
selection: selection,
markdownText: trimmedMarkdownText,
final nodes = editorState.getNodesInSelection(selection);
final transaction = editorState.transaction..deleteNodes(nodes);
await editorState.apply(
transaction,
withUpdateSelection: false,
);
await exit(withDiscard: false, withUnformat: false);
return;
}
@@ -303,35 +277,30 @@ class AiWriterCubit extends Cubit<AiWriterState> {
AiWriterCommand command,
) async {
final node = aiWriterNode;
// check the node is registered
if (node == null) {
Log.warn('[AI writer] Node is null');
return (false, '');
}
// check the selection is valid
final selection = node.aiWriterSelection?.normalized;
if (selection == null) {
Log.warn('[AI writer]Selection is null');
return (false, '');
}
// if the command is continue writing, we don't need to get the selection text
if (command == AiWriterCommand.continueWriting) {
return (true, '');
}
// if the selection is collapsed, we don't need to get the selection text
if (selection.isCollapsed) {
return (true, '');
}
final selectionText = await editorState.getMarkdownInSelection(selection);
Log.warn('[AI writer] Selection is null');
if (command == AiWriterCommand.userQuestion) {
records.add(
AiWriterRecord.user(content: selectionText, format: null),
);
return (true, '');
} else {
return (true, selectionText);
@@ -575,10 +544,6 @@ class AiWriterCubit extends Cubit<AiWriterState> {
attributes: ApplySuggestionFormatType.replace.attributes,
);
onAppendToDocument?.call();
_aiWriterCubitLog(
'received message: $text',
);
},
processAssistMessage: (text) async {
if (state case final GeneratingAiWriterState generatingState) {
@@ -590,10 +555,6 @@ class AiWriterCubit extends Cubit<AiWriterState> {
),
);
}
_aiWriterCubitLog(
'received assist message: $text',
);
},
onEnd: () async {
if (state case final GeneratingAiWriterState generatingState) {
@@ -610,10 +571,6 @@ class AiWriterCubit extends Cubit<AiWriterState> {
records.add(
AiWriterRecord.ai(content: _textRobot.markdownText),
);
_aiWriterCubitLog(
'returned response: ${_textRobot.markdownText}',
);
}
},
onError: (error) async {
@@ -705,12 +662,6 @@ class AiWriterCubit extends Cubit<AiWriterState> {
);
}
}
void _aiWriterCubitLog(String message) {
if (_aiWriterCubitDebugLog) {
Log.debug('[AiWriterCubit] $message');
}
}
}
mixin RegisteredAiWriter {
@@ -34,15 +34,7 @@ extension AiWriterNodeExtension on EditorState {
// if the selected nodes are not entirely selected, slice the nodes
final slicedNodes = <Node>[];
final List<Node> flattenNodes = getNodesInSelection(selection);
final List<Node> nodes = [];
for (final node in flattenNodes) {
if (nodes.any((element) => element.isParentOf(node))) {
continue;
}
nodes.add(node);
}
final nodes = getNodesInSelection(selection);
for (final node in nodes) {
final delta = node.delta;
@@ -65,30 +57,11 @@ extension AiWriterNodeExtension on EditorState {
slicedNodes.add(copiedNode);
}
for (final (i, node) in slicedNodes.indexed) {
final childNodesShouldBeDeleted = <Node>[];
for (final child in node.children) {
if (!child.path.inSelection(selection)) {
childNodesShouldBeDeleted.add(child);
}
}
for (final child in childNodesShouldBeDeleted) {
slicedNodes[i] = node.copyWith(
children: node.children.where((e) => e.id != child.id).toList(),
type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type,
);
}
}
// use \n\n as line break to improve the ai response
// using \n will cause the ai response treat the text as a single line
final markdown = await customDocumentToMarkdown(
Document.blank()..insert([0], slicedNodes),
lineBreak: '\n',
);
// trim the last \n if it exists
return markdown.trimRight();
return markdown;
}
List<String> getPlainTextInSelection(Selection? selection) {
@@ -40,7 +40,6 @@ class _AiWriterScrollWrapperState extends State<AiWriterScrollWrapper> {
onRemoveNode: () {
aiWriterRegistered = false;
widget.editorState.service.keyboardService?.enableShortcuts();
widget.editorState.service.keyboardService?.enable();
},
onAppendToDocument: onAppendToDocument,
);
@@ -209,26 +208,11 @@ class _AiWriterScrollWrapperState extends State<AiWriterScrollWrapper> {
throttler.call(() {
if (aiWriterCubit.aiWriterNode != null) {
final path = aiWriterCubit.aiWriterNode!.path;
if (path.isEmpty) {
return;
if (path.isNotEmpty) {
widget.editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: path)),
);
}
if (path.previous.isNotEmpty) {
final node = widget.editorState.getNodeAtPath(path.previous);
if (node != null && node.delta != null && node.delta!.isNotEmpty) {
widget.editorState.updateSelectionWithReason(
Selection.collapsed(
Position(path: path, offset: node.delta!.length),
),
);
return;
}
}
widget.editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: path)),
);
}
});
}
@@ -1,4 +1,3 @@
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
@@ -7,7 +6,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class BuiltInPageWidget extends StatefulWidget {
const BuiltInPageWidget({
@@ -79,9 +77,6 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
}
Widget _buildPage(BuildContext context, ViewPB view) {
final verticalPadding =
context.read<DatabasePluginWidgetBuilderSize?>()?.verticalPadding ??
0.0;
return Focus(
focusNode: focusNode,
onFocusChange: (value) {
@@ -90,7 +85,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
}
},
child: Padding(
padding: EdgeInsets.symmetric(vertical: verticalPadding),
padding: const EdgeInsets.symmetric(vertical: 16),
child: widget.builder(view),
),
);
@@ -70,12 +70,13 @@ extension InsertDatabase on EditorState {
node,
selection.end.offset,
0,
MentionBlockKeys.mentionChar,
attributes: MentionBlockKeys.buildMentionPageAttributes(
mentionType: MentionType.page,
pageId: view.id,
blockId: null,
),
r'$',
attributes: {
MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionType.page.name,
MentionBlockKeys.pageId: view.id,
},
},
);
}
@@ -110,15 +110,12 @@ class MarkdownTextRobot {
}
/// Persist the text into the document
Future<void> persist({
String? markdownText,
}) async {
Future<void> persist({String? markdownText}) async {
if (markdownText != null) {
_markdownText = markdownText;
}
await _lock.synchronized(() async {
await _refresh(inMemoryUpdate: false, updateSelection: true);
await _refresh(inMemoryUpdate: false);
});
if (_enableDebug) {
@@ -127,38 +124,8 @@ class MarkdownTextRobot {
}
}
/// Replace the selected content with the AI's response
Future<void> replace({
required Selection selection,
required String markdownText,
}) async {
if (selection.isSingle) {
await _replaceInSameLine(
selection: selection,
markdownText: markdownText,
);
} else {
await _replaceInMultiLines(
selection: selection,
markdownText: markdownText,
);
}
}
/// Delete the temporary inserted AI nodes
Future<void> deleteAINodes() async {
final nodes = getInsertedNodes();
final transaction = editorState.transaction..deleteNodes(nodes);
await editorState.apply(
transaction,
options: const ApplyOptions(recordUndo: false),
);
}
/// Discard the inserted content
Future<void> discard({
Selection? afterSelection,
}) async {
Future<void> discard() async {
final start = _insertPosition;
if (start == null) {
return;
@@ -167,8 +134,6 @@ class MarkdownTextRobot {
return;
}
afterSelection ??= Selection.collapsed(start);
// fallback to the calculated position if the selection is null.
final end = Position(
path: start.path.nextNPath(_insertedNodes.length - 1),
@@ -178,7 +143,7 @@ class MarkdownTextRobot {
);
final transaction = editorState.transaction
..deleteNodes(deletedNodes)
..afterSelection = afterSelection;
..afterSelection = Selection.collapsed(start);
await editorState.apply(
transaction,
@@ -317,242 +282,6 @@ class MarkdownTextRobot {
children: children,
);
}
/// If the selected content is in the same line,
/// keep the selected node and replace the delta.
Future<void> _replaceInSameLine({
required Selection selection,
required String markdownText,
}) async {
if (markdownText.isEmpty) {
assert(false, 'Expected non-empty markdown text');
Log.error('Expected non-empty markdown text');
return;
}
selection = selection.normalized;
// If the selection is not a single node, do nothing.
if (!selection.isSingle) {
assert(false, 'Expected single node selection');
Log.error('Expected single node selection');
return;
}
final startIndex = selection.startIndex;
final endIndex = selection.endIndex;
final length = endIndex - startIndex;
// Get the selected node.
final node = editorState.getNodeAtPath(selection.start.path);
final delta = node?.delta;
if (node == null || delta == null) {
assert(false, 'Expected non-null node and delta');
Log.error('Expected non-null node and delta');
return;
}
// Convert the markdown text to delta.
// Question: Why we need to convert the markdown to document first?
// Answer: Because the markdown text may contain the list item,
// if we convert the markdown to delta directly, the list item will be
// treated as a normal text node, and the delta will be incorrect.
// For example, the markdown text is:
// ```
// 1. item1
// ```
// if we convert the markdown to delta directly, the delta will be:
// ```
// [
// {
// "insert": "1. item1"
// }
// ]
// ```
// if we convert the markdown to document first, the document will be:
// ```
// [
// {
// "type": "numbered_list",
// "children": [
// {
// "insert": "item1"
// }
// ]
// }
// ]
final document = customMarkdownToDocument(markdownText);
final nodes = document.root.children;
final decoder = DeltaMarkdownDecoder();
final markdownDelta =
nodes.firstOrNull?.delta ?? decoder.convert(markdownText);
if (markdownDelta.isEmpty) {
assert(false, 'Expected non-empty markdown delta');
Log.error('Expected non-empty markdown delta');
return;
}
// Replace the delta of the selected node.
final transaction = editorState.transaction;
// it means the user selected the entire sentence, we just replace the node
if (startIndex == 0 && length == node.delta?.length) {
if (nodes.isNotEmpty && node.children.isNotEmpty) {
// merge the children of the selected node and the first node of the ai response
nodes[0] = nodes[0].copyWith(
children: [
...node.children.map((e) => e.deepCopy()),
...nodes[0].children,
],
);
}
transaction
..insertNodes(node.path.next, nodes)
..deleteNode(node);
} else {
// it means the user selected a part of the sentence, we need to delete the
// selected part and insert the new delta.
transaction
..deleteText(node, startIndex, length)
..insertTextDelta(node, startIndex, markdownDelta);
// Add the remaining nodes to the document.
final remainingNodes = nodes.skip(1);
if (remainingNodes.isNotEmpty) {
transaction.insertNodes(
node.path.next,
remainingNodes,
);
}
}
await editorState.apply(transaction);
}
/// If the selected content is in multiple lines
Future<void> _replaceInMultiLines({
required Selection selection,
required String markdownText,
}) async {
selection = selection.normalized;
// If the selection is a single node, do nothing.
if (selection.isSingle) {
assert(false, 'Expected multi-line selection');
Log.error('Expected multi-line selection');
return;
}
final markdownNodes = customMarkdownToDocument(
markdownText,
tableWidth: 250.0,
).root.children;
// Get the selected nodes.
final flattenNodes = editorState.getNodesInSelection(selection);
final nodes = <Node>[];
for (final node in flattenNodes) {
if (nodes.any((element) => element.isParentOf(node))) {
continue;
}
nodes.add(node);
}
// Note: Don't change its order, otherwise the delta will be incorrect.
// step 1. merge the first selected node and the first node from the ai response
// step 2. merge the last selected node and the last node from the ai response
// step 3. insert the middle nodes from the ai response
// step 4. delete the middle nodes
final transaction = editorState.transaction;
// step 1
final firstNode = nodes.firstOrNull;
final delta = firstNode?.delta;
final firstMarkdownNode = markdownNodes.firstOrNull;
final firstMarkdownDelta = firstMarkdownNode?.delta;
if (firstNode != null &&
delta != null &&
firstMarkdownNode != null &&
firstMarkdownDelta != null) {
final startIndex = selection.startIndex;
final length = delta.length - startIndex;
transaction
..deleteText(firstNode, startIndex, length)
..insertTextDelta(firstNode, startIndex, firstMarkdownDelta);
// if the first markdown node has children, we need to insert the children
// and delete the children of the first node that are in the selection.
if (firstMarkdownNode.children.isNotEmpty) {
transaction.insertNodes(
firstNode.path.child(0),
firstMarkdownNode.children.map((e) => e.deepCopy()),
);
}
final nodesToDelete =
firstNode.children.where((e) => e.path.inSelection(selection));
transaction.deleteNodes(nodesToDelete);
}
// step 2
bool handledLastNode = false;
final lastNode = nodes.lastOrNull;
final lastDelta = lastNode?.delta;
final lastMarkdownNode = markdownNodes.lastOrNull;
final lastMarkdownDelta = lastMarkdownNode?.delta;
if (lastNode != null &&
lastDelta != null &&
lastMarkdownNode != null &&
lastMarkdownDelta != null &&
firstNode?.id != lastNode.id) {
handledLastNode = true;
final endIndex = selection.endIndex;
transaction.deleteText(lastNode, 0, endIndex);
// if the last node is same as the first node, it means we have replaced the
// selected text in the first node.
if (lastMarkdownNode.id != firstMarkdownNode?.id) {
transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta);
if (lastMarkdownNode.children.isNotEmpty) {
transaction
..insertNodes(
lastNode.path.child(0),
lastMarkdownNode.children.map((e) => e.deepCopy()),
)
..deleteNodes(
lastNode.children.where((e) => e.path.inSelection(selection)),
);
}
}
}
// step 3
final insertedPath = selection.start.path.nextNPath(1);
final insertLength = handledLastNode ? 2 : 1;
if (markdownNodes.length > insertLength) {
transaction.insertNodes(
insertedPath,
markdownNodes
.skip(1)
.take(markdownNodes.length - insertLength)
.toList(),
);
}
// step 4
final length = nodes.length - 2;
if (length > 0) {
final middleNodes = nodes.skip(1).take(length).toList();
transaction.deleteNodes(middleNodes);
}
await editorState.apply(transaction);
}
}
class AINodeExternalValues extends NodeExternalValues {
@@ -47,6 +47,7 @@ class _CopyButton extends StatelessWidget {
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
);
}
@@ -1,4 +1,3 @@
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
@@ -116,11 +115,7 @@ class SimpleColumnBlockComponentState extends State<SimpleColumnBlockComponent>
crossAxisAlignment: CrossAxisAlignment.start,
children: node.children.map(
(e) {
Widget child = Provider(
create: (_) => DatabasePluginWidgetBuilderSize(
verticalPadding: 0,
horizontalPadding: 0,
),
Widget child = IntrinsicHeight(
child: editorState.renderer.build(context, e),
);
if (SimpleColumnsBlockConstants.enableDebugBorder) {
@@ -9,12 +9,10 @@ class SimpleColumnBlockWidthResizer extends StatefulWidget {
super.key,
required this.columnNode,
required this.editorState,
this.height,
});
final Node columnNode;
final EditorState editorState;
final double? height;
@override
State<SimpleColumnBlockWidthResizer> createState() =>
@@ -55,14 +53,15 @@ class _SimpleColumnBlockWidthResizerState
child: ValueListenableBuilder<bool>(
valueListenable: isHovering,
builder: (context, isHovering, child) {
final hide = isDraggingAppFlowyEditorBlock.value || !isHovering;
if (isDraggingAppFlowyEditorBlock.value) {
return SizedBox.shrink();
}
return MouseRegion(
cursor: SystemMouseCursors.resizeLeftRight,
child: Container(
width: 2,
height: widget.height ?? 20,
margin: EdgeInsets.symmetric(horizontal: 2),
color: !hide
color: isHovering
? Theme.of(context).colorScheme.primary
: Colors.transparent,
),
@@ -92,18 +92,17 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
final ScrollController scrollController = ScrollController();
final ValueNotifier<double?> heightValueNotifier = ValueNotifier(null);
@override
void initState() {
super.initState();
_updateColumnsBlock();
}
@override
void dispose() {
scrollController.dispose();
heightValueNotifier.dispose();
super.dispose();
}
@@ -111,13 +110,15 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
Widget build(BuildContext context) {
Widget child = Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildChildren(),
);
child = Align(
alignment: Alignment.topLeft,
child: child,
child: IntrinsicHeight(
child: child,
),
);
child = Padding(
@@ -140,10 +141,7 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
// the columns block does not support the block actions and selection
// because the columns block is a layout wrapper, it does not have a content
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (v) => updateHeightValueNotifier(v),
child: SizeChangedLayoutNotifier(child: child),
);
return child;
}
List<Widget> _buildChildren() {
@@ -166,15 +164,9 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
if (i != length - 1) {
children.add(
ValueListenableBuilder(
valueListenable: heightValueNotifier,
builder: (context, height, child) {
return SimpleColumnBlockWidthResizer(
columnNode: childNode,
editorState: editorState,
height: height,
);
},
SimpleColumnBlockWidthResizer(
columnNode: childNode,
editorState: editorState,
),
);
}
@@ -202,16 +194,6 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
}
}
bool updateHeightValueNotifier(SizeChangedLayoutNotification notification) {
if (!mounted) return true;
final height = _renderBox?.size.height;
if (heightValueNotifier.value == height) return true;
WidgetsBinding.instance.addPostFrameCallback((_) {
heightValueNotifier.value = height;
});
return true;
}
@override
Position start() => Position(path: widget.node.path);
@@ -16,7 +16,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:http/http.dart' as http;
import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
/// - support
/// - desktop
@@ -163,7 +162,6 @@ Future<bool> _pasteAsLinkPreview(
EditorState editorState,
String? text,
) async {
final isMobile = UniversalPlatform.isMobile;
// the url should contain a protocol
if (text == null || !isURL(text, {'require_protocol': true})) {
return false;
@@ -186,7 +184,7 @@ Future<bool> _pasteAsLinkPreview(
node.delta?.toPlainText().isNotEmpty == true) {
return false;
}
if (!isMobile) return false;
final bool isImageUrl;
try {
isImageUrl = await _isImageUrl(text);
@@ -195,8 +193,6 @@ Future<bool> _pasteAsLinkPreview(
return false;
}
if (!isImageUrl) return false;
// insert the text with link format
final textTransaction = editorState.transaction
..insertText(
@@ -254,7 +250,6 @@ Future<void> doPlainPaste(EditorState editorState) async {
}
Future<bool> _isImageUrl(String text) async {
if (isNotImageUrl(text)) return false;
final response = await http.head(Uri.parse(text));
if (response.statusCode == 200) {
@@ -43,11 +43,13 @@ extension PasteFromBlockLink on EditorState {
node,
selection.startIndex,
MentionBlockKeys.mentionChar,
attributes: MentionBlockKeys.buildMentionPageAttributes(
mentionType: MentionType.page,
pageId: pageId,
blockId: blockId,
),
attributes: {
MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionType.page.name,
MentionBlockKeys.blockId: blockId,
MentionBlockKeys.pageId: pageId,
},
},
);
await apply(transaction);
@@ -1,4 +1,3 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
@@ -14,7 +13,6 @@ extension PasteFromHtml on EditorState {
}
if (nodes.length == 1) {
await pasteSingleLineNode(nodes.first);
checkToShowPasteAsMenu(nodes.first);
} else {
await pasteMultiLineNodes(nodes.toList());
}
@@ -73,6 +73,7 @@ extension PasteFromImage on EditorState {
Log.info('unsupported format: $format');
if (UniversalPlatform.isMobile) {
showToastNotification(
context,
message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(),
);
}
@@ -111,6 +112,7 @@ extension PasteFromImage on EditorState {
if (errorMessage != null && context.mounted) {
showToastNotification(
context,
message: errorMessage,
);
return false;
@@ -129,6 +131,7 @@ extension PasteFromImage on EditorState {
Log.error('cannot copy image file', e);
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.document_imageBlock_error_invalidImage.tr(),
);
}
@@ -1,8 +1,6 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:universal_platform/universal_platform.dart';
extension PasteFromPlainText on EditorState {
Future<void> pastePlainText(String plainText) async {
@@ -43,7 +41,6 @@ extension PasteFromPlainText on EditorState {
}
if (nodes.length == 1) {
await pasteSingleLineNode(nodes.first);
checkToShowPasteAsMenu(nodes.first);
} else {
await pasteMultiLineNodes(nodes.toList());
}
@@ -68,29 +65,6 @@ extension PasteFromPlainText on EditorState {
AppFlowyRichTextKeys.href: plainText,
});
await apply(transaction);
checkToShowPasteAsMenu(node);
return true;
}
void checkToShowPasteAsMenu(Node node) {
if (selection == null || !selection!.isCollapsed) return;
if (UniversalPlatform.isMobile) return;
final href = _getLinkFromNode(node);
if (href != null) {
final context = document.root.context;
if (context != null && context.mounted) {
PasteAsMenuService(context: context, editorState: this).show(href);
}
}
}
String? _getLinkFromNode(Node node) {
final delta = node.delta;
if (delta == null) return null;
final inserts = delta.whereType<TextInsert>();
if (inserts.isEmpty || inserts.length > 1) return null;
final link = inserts.first.attributes?.href;
if (link != null) return inserts.first.text;
return null;
}
}
@@ -1,4 +1,3 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -28,15 +27,9 @@ extension TextDeltaExtension on Delta {
if (op.text == MentionBlockKeys.mentionChar) {
final mention = attributes?[MentionBlockKeys.mention];
final mentionPageId = mention?[MentionBlockKeys.pageId];
final mentionType = mention?[MentionBlockKeys.type];
if (mentionPageId != null) {
text += await getMentionPageName(mentionPageId);
continue;
} else if (mentionType == MentionType.externalLink.name) {
final url = mention?[MentionBlockKeys.url] ?? '';
final info = await LinkInfoCache.get(url);
text += info?.title ?? url;
continue;
}
}
@@ -1,9 +1,10 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'toolbar_animation.dart';
import 'toolbar_cubit.dart';
class DesktopFloatingToolbar extends StatefulWidget {
const DesktopFloatingToolbar({
@@ -27,7 +28,6 @@ class _DesktopFloatingToolbarState extends State<DesktopFloatingToolbar> {
EditorState get editorState => widget.editorState;
_Position? position;
final toolbarController = getIt<FloatingToolbarController>();
@override
void initState() {
@@ -39,32 +39,24 @@ class _DesktopFloatingToolbarState extends State<DesktopFloatingToolbar> {
final selectionRect = editorState.selectionRects();
if (selectionRect.isEmpty) return;
position = calculateSelectionMenuOffset(selectionRect.first);
toolbarController._addCallback(dismiss);
}
@override
void dispose() {
toolbarController._removeCallback(dismiss);
super.dispose();
}
@override
Widget build(BuildContext context) {
if (position == null) return Container();
return Positioned(
left: position!.left,
top: position!.top,
right: position!.right,
child: widget.enableAnimation
? ToolbarAnimationWidget(child: widget.child)
: widget.child,
return BlocProvider<ToolbarCubit>(
create: (_) => ToolbarCubit(widget.onDismiss),
child: Positioned(
left: position!.left,
top: position!.top,
right: position!.right,
child: widget.enableAnimation
? ToolbarAnimationWidget(child: widget.child)
: widget.child,
),
);
}
void dismiss() {
widget.onDismiss.call();
}
_Position calculateSelectionMenuOffset(
Rect rect,
) {
@@ -100,33 +92,3 @@ class _Position {
final double? top;
final double? right;
}
class FloatingToolbarController {
final Set<VoidCallback> _dismissCallbacks = {};
final Set<VoidCallback> _displayListeners = {};
void _addCallback(VoidCallback callback) {
_dismissCallbacks.add(callback);
for (final listener in Set.of(_displayListeners)) {
listener.call();
}
}
void _removeCallback(VoidCallback callback) =>
_dismissCallbacks.remove(callback);
bool get isToolbarShowing => _dismissCallbacks.isNotEmpty;
void addDisplayListener(VoidCallback listener) =>
_displayListeners.add(listener);
void removeDisplayListener(VoidCallback listener) =>
_displayListeners.remove(listener);
void hideToolbar() {
if (_dismissCallbacks.isEmpty) return;
for (final callback in _dismissCallbacks) {
callback.call();
}
}
}
@@ -5,7 +5,6 @@ import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_ui/appflowy_ui.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -20,15 +19,11 @@ class LinkCreateMenu extends StatefulWidget {
required this.onSubmitted,
required this.onDismiss,
required this.alignment,
required this.currentViewId,
required this.initialText,
});
final EditorState editorState;
final void Function(String link, bool isPage) onSubmitted;
final VoidCallback onDismiss;
final String currentViewId;
final String initialText;
final LinkMenuAlignment alignment;
@override
@@ -37,8 +32,6 @@ class LinkCreateMenu extends StatefulWidget {
class _LinkCreateMenuState extends State<LinkCreateMenu> {
late LinkSearchTextField searchTextField = LinkSearchTextField(
currentViewId: widget.currentViewId,
initialSearchText: widget.initialText,
onEnter: () {
searchTextField.onSearchResult(
onLink: () => onSubmittedLink(),
@@ -55,28 +48,17 @@ class _LinkCreateMenuState extends State<LinkCreateMenu> {
},
);
bool get isTextfieldEnable => searchTextField.isTextfieldEnable;
bool get isButtonEnable => searchText.isNotEmpty;
String get searchText => searchTextField.searchText;
bool get showAtTop => widget.alignment.isTop;
bool showErrorText = false;
@override
void initState() {
super.initState();
searchTextField.requestFocus();
searchTextField.searchRecentViews();
final focusNode = searchTextField.focusNode;
bool hasFocus = focusNode.hasFocus;
focusNode.addListener(() {
if (hasFocus != focusNode.hasFocus && mounted) {
setState(() {
hasFocus = focusNode.hasFocus;
});
}
});
}
@override
@@ -116,45 +98,32 @@ class _LinkCreateMenuState extends State<LinkCreateMenu> {
Widget buildSearchContainer() {
return Container(
width: 320,
height: 48,
decoration: buildToolbarLinkDecoration(context),
padding: EdgeInsets.all(8),
child: ValueListenableBuilder(
valueListenable: searchTextField.textEditingController,
builder: (context, _, __) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
return Row(
children: [
Row(
children: [
Expanded(
child: searchTextField.buildTextField(context: context),
),
HSpace(8),
FlowyTextButton(
LocaleKeys.document_toolbar_insert.tr(),
mainAxisAlignment: MainAxisAlignment.center,
padding: EdgeInsets.zero,
constraints: BoxConstraints(maxWidth: 72, minHeight: 32),
fontSize: 14,
fontColor: Colors.white,
fillColor: LinkStyle.fillThemeThick,
hoverColor: LinkStyle.fillThemeThick.withAlpha(200),
lineHeight: 20 / 14,
fontWeight: FontWeight.w600,
onPressed: onSubmittedLink,
),
],
Expanded(child: searchTextField.buildTextField()),
HSpace(8),
FlowyTextButton(
LocaleKeys.document_toolbar_insert.tr(),
mainAxisAlignment: MainAxisAlignment.center,
padding: EdgeInsets.zero,
constraints: BoxConstraints(maxWidth: 72, minHeight: 32),
fontSize: 14,
fontColor:
isButtonEnable ? Colors.white : LinkStyle.textTertiary,
fillColor: isButtonEnable
? LinkStyle.fillThemeThick
: LinkStyle.borderColor,
hoverColor: LinkStyle.fillThemeThick,
lineHeight: 20 / 14,
fontWeight: FontWeight.w600,
onPressed: isButtonEnable ? () => onSubmittedLink() : null,
),
if (showErrorText)
Padding(
padding: const EdgeInsets.only(top: 4),
child: FlowyText.regular(
LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),
color: LinkStyle.textStatusError,
fontSize: 12,
figmaLineHeight: 16,
),
),
],
);
},
@@ -162,15 +131,7 @@ class _LinkCreateMenuState extends State<LinkCreateMenu> {
);
}
void onSubmittedLink() {
if (!isTextfieldEnable) {
setState(() {
showErrorText = true;
});
return;
}
widget.onSubmitted(searchText, false);
}
void onSubmittedLink() => widget.onSubmitted(searchText, false);
void onSubmittedPageLink(ViewPB view) async {
final workspaceId = context
@@ -191,16 +152,13 @@ void showLinkCreateMenu(
BuildContext context,
EditorState editorState,
Selection selection,
String currentViewId,
) {
if (!context.mounted) return;
final (left, top, right, bottom, alignment) = _getPosition(editorState);
final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
final selectedText = editorState.getTextInSelection(selection).join();
OverlayEntry? overlay;
@@ -220,18 +178,12 @@ void showLinkCreateMenu(
builder: (context) {
return LinkCreateMenu(
alignment: alignment,
initialText: selectedText,
currentViewId: currentViewId,
editorState: editorState,
onSubmitted: (link, isPage) async {
await editorState.formatDelta(selection, {
BuiltInAttributeKey.href: link,
kIsPageLink: isPage,
});
await editorState.updateSelectionWithReason(
null,
reason: SelectionUpdateReason.uiEvent,
);
dismissOverlay();
},
onDismiss: dismissOverlay,
@@ -305,16 +257,17 @@ void showLinkCreateMenu(
return (left, top, right, bottom, alignment);
}
ShapeDecoration buildToolbarLinkDecoration(
BuildContext context, {
double radius = 12.0,
}) {
final theme = AppFlowyTheme.of(context);
return ShapeDecoration(
color: theme.surfaceColorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radius),
),
shadows: [theme.shadow.small],
);
}
ShapeDecoration buildToolbarLinkDecoration(BuildContext context) =>
ShapeDecoration(
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shadows: [
const BoxShadow(
color: LinkStyle.shadowMedium,
blurRadius: 24,
offset: Offset(0, 4),
),
],
);
@@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -11,9 +10,7 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// ignore: implementation_imports
import 'package:appflowy_editor/src/editor/util/link_util.dart';
import 'package:flutter/services.dart';
import 'link_create_menu.dart';
import 'link_search_text_field.dart';
import 'link_styles.dart';
@@ -25,33 +22,34 @@ class LinkEditMenu extends StatefulWidget {
required this.onDismiss,
required this.onApply,
required this.onRemoveLink,
required this.currentViewId,
});
final LinkInfo linkInfo;
final ValueChanged<LinkInfo> onApply;
final ValueChanged<LinkInfo> onRemoveLink;
final VoidCallback onRemoveLink;
final VoidCallback onDismiss;
final String currentViewId;
@override
State<LinkEditMenu> createState() => _LinkEditMenuState();
}
class _LinkEditMenuState extends State<LinkEditMenu> {
ValueChanged<LinkInfo> get onRemoveLink => widget.onRemoveLink;
ValueChanged<LinkInfo> get onApply => widget.onApply;
VoidCallback get onRemoveLink => widget.onRemoveLink;
VoidCallback get onDismiss => widget.onDismiss;
late TextEditingController linkNameController =
TextEditingController(text: linkInfo.name);
late FocusNode textFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent);
late FocusNode menuFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent);
final textFocusNode = FocusNode();
late LinkInfo linkInfo = widget.linkInfo;
late LinkSearchTextField searchTextField;
bool isShowingSearchResult = false;
ViewPB? currentView;
bool showErrorText = false;
bool get enableApply =>
linkInfo.link.isNotEmpty && linkNameController.text.isNotEmpty;
@override
void initState() {
@@ -60,9 +58,18 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
if (isPageLink) getPageView();
searchTextField = LinkSearchTextField(
initialSearchText: isPageLink ? '' : linkInfo.link,
initialViewId: linkInfo.viewId,
currentViewId: widget.currentViewId,
onEnter: onConfirm,
onEnter: () {
searchTextField.onSearchResult(
onLink: onLinkSelected,
onRecentViews: () =>
onPageSelected(searchTextField.currentRecentView),
onSearchViews: () =>
onPageSelected(searchTextField.currentSearchedView),
onEmpty: () {
searchTextField.unfocus();
},
);
},
onEscape: () {
if (isShowingSearchResult) {
hideSearchResult();
@@ -74,14 +81,12 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
if (mounted) setState(() {});
},
)..searchRecentViews();
makeSureHasFocus();
}
@override
void dispose() {
linkNameController.dispose();
textFocusNode.dispose();
menuFocusNode.dispose();
searchTextField.dispose();
super.dispose();
}
@@ -90,62 +95,59 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
Widget build(BuildContext context) {
final showingRecent =
searchTextField.showingRecent && isShowingSearchResult;
final errorHeight = showErrorText ? 20.0 : 0.0;
return GestureDetector(
onTap: onDismiss,
child: Focus(
focusNode: menuFocusNode,
child: Container(
width: 400,
height: 250 + (showingRecent ? 32 : 0),
color: Colors.white.withAlpha(1),
child: Stack(
children: [
GestureDetector(
onTap: hideSearchResult,
child: Container(
width: 400,
height: 192 + errorHeight,
decoration: buildToolbarLinkDecoration(context),
),
child: Container(
width: 400,
height: 250 + (showingRecent ? 32 : 0),
color: Colors.white.withAlpha(1),
child: Stack(
children: [
GestureDetector(
onTap: hideSearchResult,
child: Container(
width: 400,
height: 192,
decoration: buildToolbarLinkDecoration(context),
padding: EdgeInsets.fromLTRB(20, 16, 20, 16),
),
Positioned(
top: 16,
left: 20,
child: FlowyText.semibold(
LocaleKeys.document_toolbar_pageOrURL.tr(),
color: LinkStyle.textTertiary,
fontSize: 12,
figmaLineHeight: 16,
),
),
Positioned(
top: 16,
left: 20,
child: FlowyText.semibold(
LocaleKeys.document_toolbar_pageOrURL.tr(),
color: LinkStyle.textTertiary,
fontSize: 12,
figmaLineHeight: 16,
),
Positioned(
top: 80 + errorHeight,
left: 20,
child: FlowyText.semibold(
LocaleKeys.document_toolbar_linkName.tr(),
color: LinkStyle.textTertiary,
fontSize: 12,
figmaLineHeight: 16,
),
),
Positioned(
top: 80,
left: 20,
child: FlowyText.semibold(
LocaleKeys.document_toolbar_linkName.tr(),
color: LinkStyle.textTertiary,
fontSize: 12,
figmaLineHeight: 16,
),
Positioned(
top: 144 + errorHeight,
left: 20,
child: buildButtons(),
),
Positioned(
top: 100 + errorHeight,
left: 20,
child: buildNameTextField(),
),
Positioned(
top: 36,
left: 20,
child: buildLinkField(),
),
],
),
),
Positioned(
top: 152,
left: 20,
child: buildButtons(),
),
Positioned(
top: 108,
left: 20,
child: buildNameTextField(),
),
Positioned(
top: 36,
left: 20,
child: buildLinkField(),
),
],
),
),
);
@@ -153,53 +155,29 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
Widget buildLinkField() {
final showPageView = linkInfo.isPage && !isShowingSearchResult;
Widget child;
if (showPageView) {
child = buildPageView();
} else if (!isShowingSearchResult) {
child = buildLinkView();
} else {
return SizedBox(
width: 360,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 360,
height: 32,
child: searchTextField.buildTextField(
autofocus: true,
context: context,
),
),
VSpace(6),
searchTextField.buildResultContainer(
context: context,
width: 360,
onPageLinkSelected: onPageSelected,
onLinkSelected: onLinkSelected,
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
child,
if (showErrorText)
Padding(
padding: const EdgeInsets.only(top: 4),
child: FlowyText.regular(
LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),
color: LinkStyle.textStatusError,
fontSize: 12,
figmaLineHeight: 16,
if (showPageView) return buildPageView();
if (!isShowingSearchResult) return buildLinkView();
return SizedBox(
width: 360,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 360,
height: 32,
child: searchTextField.buildTextField(
autofocus: true,
),
),
],
VSpace(6),
searchTextField.buildResultContainer(
context: context,
width: 360,
onPageLinkSelected: onPageSelected,
onLinkSelected: onLinkSelected,
),
],
),
);
}
@@ -219,15 +197,15 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
preferBelow: false,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8)),
border: Border.all(color: LinkStyle.borderColor(context)),
border: Border.all(color: LinkStyle.borderColor),
),
onPressed: () => onRemoveLink.call(linkInfo),
onPressed: onRemoveLink,
),
Spacer(),
DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8)),
border: Border.all(color: LinkStyle.borderColor(context)),
border: Border.all(color: LinkStyle.borderColor),
),
child: FlowyTextButton(
LocaleKeys.button_cancel.tr(),
@@ -236,9 +214,7 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
constraints: BoxConstraints(maxWidth: 78, minHeight: 32),
fontSize: 14,
lineHeight: 20 / 14,
fontColor: Theme.of(context).isLightMode
? LinkStyle.textPrimary
: Theme.of(context).iconTheme.color,
fontColor: LinkStyle.textPrimary,
fillColor: Colors.transparent,
fontWeight: FontWeight.w400,
onPressed: onDismiss,
@@ -256,10 +232,14 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
fontSize: 14,
lineHeight: 20 / 14,
hoverColor: LinkStyle.fillThemeThick.withAlpha(200),
fontColor: Colors.white,
fillColor: LinkStyle.fillThemeThick,
fontColor:
enableApply ? Colors.white : LinkStyle.textTertiary,
fillColor: enableApply
? LinkStyle.fillThemeThick
: LinkStyle.borderColor,
fontWeight: FontWeight.w400,
onPressed: onApply,
onPressed:
enableApply ? () => widget.onApply.call(linkInfo) : null,
);
},
),
@@ -276,7 +256,6 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
child: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
focusNode: textFocusNode,
autofocus: true,
textAlign: TextAlign.left,
controller: linkNameController,
style: TextStyle(
@@ -293,7 +272,6 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
},
decoration: LinkStyle.buildLinkTextFieldInputDecoration(
LocaleKeys.document_toolbar_linkNameHint.tr(),
context,
),
),
);
@@ -310,34 +288,24 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
),
);
} else {
final viewName = view.name;
final displayName = viewName.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: viewName;
child = GestureDetector(
onTap: showSearchResult,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: FlowyTooltip(
preferBelow: false,
message: displayName,
child: Container(
height: 32,
padding: EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Row(
children: [
searchTextField.buildIcon(view),
HSpace(4),
Flexible(
child: FlowyText.regular(
displayName,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 20,
fontSize: 14,
),
child: Container(
height: 32,
padding: EdgeInsets.fromLTRB(8, 6, 8, 6),
child: Row(
children: [
searchTextField.buildIcon(view),
HSpace(8),
Flexible(
child: FlowyText.regular(
view.name,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
@@ -356,28 +324,24 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
width: 360,
height: 32,
decoration: buildDecoration(),
child: FlowyTooltip(
preferBelow: false,
message: linkInfo.link,
child: GestureDetector(
onTap: showSearchResult,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 6, 8, 6),
child: Row(
children: [
FlowySvg(FlowySvgs.toolbar_link_earth_m),
HSpace(8),
Flexible(
child: FlowyText.regular(
linkInfo.link,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 20,
),
child: GestureDetector(
onTap: showSearchResult,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 6, 8, 6),
child: Row(
children: [
FlowySvg(FlowySvgs.toolbar_link_earth_m),
HSpace(8),
Flexible(
child: FlowyText.regular(
linkInfo.link,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 20,
),
],
),
),
],
),
),
),
@@ -385,61 +349,12 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
);
}
KeyEventResult onFocusKeyEvent(FocusNode node, KeyEvent key) {
if (key is! KeyDownEvent) return KeyEventResult.ignored;
if (key.logicalKey == LogicalKeyboardKey.enter) {
onApply();
return KeyEventResult.handled;
} else if (key.logicalKey == LogicalKeyboardKey.escape) {
onDismiss();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
Future<void> makeSureHasFocus() async {
final focusNode = textFocusNode;
if (!mounted || focusNode.hasFocus) return;
focusNode.requestFocus();
WidgetsBinding.instance.addPostFrameCallback((_) {
makeSureHasFocus();
});
}
void onApply() {
if (isShowingSearchResult) {
onConfirm();
return;
}
if (linkInfo.link.isEmpty) {
widget.onRemoveLink(linkInfo);
return;
}
if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) {
setState(() {
showErrorText = true;
});
return;
}
widget.onApply.call(linkInfo);
}
void onConfirm() {
searchTextField.onSearchResult(
onLink: onLinkSelected,
onRecentViews: () => onPageSelected(searchTextField.currentRecentView),
onSearchViews: () => onPageSelected(searchTextField.currentSearchedView),
onEmpty: () {
searchTextField.unfocus();
},
);
menuFocusNode.requestFocus();
}
Future<void> getPageView() async {
if (!linkInfo.isPage) return;
final link = linkInfo.link;
final viewId = link.split('/').lastOrNull ?? '';
final (view, isInTrash, isDeleted) =
await ViewBackendService.getMentionPageStatus(linkInfo.viewId);
await ViewBackendService.getMentionPageStatus(viewId);
if (mounted) {
setState(() {
currentView = view;
@@ -498,7 +413,7 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
BoxDecoration buildDecoration() => BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: LinkStyle.borderColor(context)),
border: Border.all(color: LinkStyle.borderColor),
);
}
@@ -511,6 +426,4 @@ class LinkInfo {
Attributes toAttribute() =>
{AppFlowyRichTextKeys.href: link, kIsPageLink: isPage};
String get viewId => isPage ? link.split('/').lastOrNull ?? '' : '';
}
@@ -3,29 +3,20 @@ import 'dart:math';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'link_create_menu.dart';
import 'link_edit_menu.dart';
import 'link_styles.dart';
class LinkHoverTrigger extends StatefulWidget {
const LinkHoverTrigger({
@@ -54,7 +45,6 @@ class LinkHoverTrigger extends StatefulWidget {
class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
final hoverMenuController = PopoverController();
final editMenuController = PopoverController();
final toolbarController = getIt<FloatingToolbarController>();
bool isHoverMenuShowing = false;
bool isHoverMenuHovering = false;
bool isHoverTriggerHovering = false;
@@ -67,13 +57,12 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
Attributes get attribute => widget.attribute;
late HoverTriggerKey triggerKey = HoverTriggerKey(widget.node.id, selection);
HoverTriggerKey get triggerKey => HoverTriggerKey(widget.node.id, selection);
@override
void initState() {
super.initState();
getIt<LinkHoverTriggers>()._add(triggerKey, showLinkHoverMenu);
toolbarController.addDisplayListener(onToolbarShow);
}
@override
@@ -81,7 +70,6 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
hoverMenuController.close();
editMenuController.close();
getIt<LinkHoverTriggers>()._remove(triggerKey, showLinkHoverMenu);
toolbarController.removeDisplayListener(onToolbarShow);
super.dispose();
}
@@ -143,11 +131,10 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
isHoverMenuHovering = false;
tryToDismissLinkHoverMenu();
},
onConvertTo: (type) => convertLinkTo(editorState, selection, type),
onOpenLink: openLink,
onCopyLink: () => copyLink(context),
onCopyLink: copyLink,
onEditLink: showLinkEditMenu,
onRemoveLink: () => removeLink(editorState, selection),
onRemoveLink: () => removeLink(editorState, selection, true),
),
child: child,
);
@@ -157,7 +144,6 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
final href = attribute.href ?? '',
isPage = attribute.isPage,
title = editorState.getTextInSelection(selection).join();
final currentViewId = context.read<DocumentBloc?>()?.documentId ?? '';
return AppFlowyPopover(
controller: editMenuController,
direction: PopoverDirection.bottomWithLeftAligned,
@@ -173,7 +159,6 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
minHeight: 282,
),
popupBuilder: (context) => LinkEditMenu(
currentViewId: currentViewId,
linkInfo: LinkInfo(name: title, link: href, isPage: isPage),
onDismiss: () => editMenuController.close(),
onApply: (info) async {
@@ -188,19 +173,14 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
editMenuController.close();
await editorState.apply(transaction);
},
onRemoveLink: (linkinfo) =>
onRemoveAndReplaceLink(editorState, selection, linkinfo.name),
onRemoveLink: () => removeLink(editorState, selection, true),
),
child: child,
);
}
void onToolbarShow() => hoverMenuController.close();
void showLinkHoverMenu() {
if (isHoverMenuShowing || toolbarController.isToolbarShowing || !mounted) {
return;
}
if (isHoverMenuShowing) return;
keepEditorFocusNotifier.increase();
hoverMenuController.show();
}
@@ -226,7 +206,7 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
if (isPage) {
final viewId = href.split('/').lastOrNull ?? '';
if (viewId.isEmpty) {
await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true);
await afLaunchUrlString(href);
} else {
final (view, isInTrash, isDeleted) =
await ViewBackendService.getMentionPageStatus(viewId);
@@ -235,20 +215,24 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
}
}
} else {
await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true);
await afLaunchUrlString(href);
}
}
Future<void> copyLink(BuildContext context) async {
Future<void> copyLink() async {
final href = widget.attribute.href ?? '';
await context.copyLink(href);
if (href.isEmpty) return;
await getIt<ClipboardService>()
.setData(ClipboardServiceData(plainText: href));
hoverMenuController.close();
}
void removeLink(
EditorState editorState,
Selection selection,
bool isHref,
) {
if (!isHref) return;
final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return;
@@ -267,54 +251,9 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
);
editorState.apply(transaction);
}
Future<void> convertLinkTo(
EditorState editorState,
Selection selection,
LinkConvertMenuCommand type,
) async {
final url = widget.attribute.href ?? '';
if (type == LinkConvertMenuCommand.toBookmark) {
await convertUrlToLinkPreview(editorState, selection, url);
} else if (type == LinkConvertMenuCommand.toMention) {
await convertUrlToMention(editorState, selection);
} else if (type == LinkConvertMenuCommand.toEmbed) {
await convertUrlToLinkPreview(
editorState,
selection,
url,
previewType: LinkEmbedKeys.embed,
);
}
}
void onRemoveAndReplaceLink(
EditorState editorState,
Selection selection,
String text,
) {
final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
final index = selection.normalized.startIndex;
final length = selection.length;
final transaction = editorState.transaction
..replaceText(
node,
index,
length,
text,
attributes: {
BuiltInAttributeKey.href: null,
kIsPageLink: null,
},
);
editorState.apply(transaction);
}
}
class LinkHoverMenu extends StatefulWidget {
class LinkHoverMenu extends StatelessWidget {
const LinkHoverMenu({
super.key,
required this.attribute,
@@ -325,7 +264,6 @@ class LinkHoverMenu extends StatefulWidget {
required this.onOpenLink,
required this.onEditLink,
required this.onRemoveLink,
required this.onConvertTo,
});
final Attributes attribute;
@@ -336,41 +274,18 @@ class LinkHoverMenu extends StatefulWidget {
final VoidCallback onOpenLink;
final VoidCallback onEditLink;
final VoidCallback onRemoveLink;
final ValueChanged<LinkConvertMenuCommand> onConvertTo;
@override
State<LinkHoverMenu> createState() => _LinkHoverMenuState();
}
class _LinkHoverMenuState extends State<LinkHoverMenu> {
ViewPB? currentView;
late bool isPage = widget.attribute.isPage;
late String href = widget.attribute.href ?? '';
final popoverController = PopoverController();
bool isConvertButtonSelected = false;
@override
void initState() {
super.initState();
if (isPage) getPageView();
}
@override
void dispose() {
super.dispose();
popoverController.close();
}
@override
Widget build(BuildContext context) {
final href = attribute.href ?? '';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MouseRegion(
onEnter: widget.onEnter,
onExit: widget.onExit,
onEnter: onEnter,
onExit: onExit,
child: SizedBox(
width: max(320, widget.triggerSize.width),
width: max(320, triggerSize.width),
height: 48,
child: Align(
alignment: Alignment.centerLeft,
@@ -378,41 +293,43 @@ class _LinkHoverMenuState extends State<LinkHoverMenu> {
width: 320,
height: 48,
decoration: buildToolbarLinkDecoration(context),
padding: EdgeInsets.fromLTRB(12, 8, 8, 8),
padding: EdgeInsets.all(8),
child: Row(
children: [
Expanded(child: buildLinkWidget()),
Expanded(
child: FlowyText.regular(
href,
fontSize: 14,
figmaLineHeight: 20,
overflow: TextOverflow.ellipsis,
),
),
Container(
height: 20,
width: 1,
color: Color(0xffE8ECF3)
.withAlpha(Theme.of(context).isLightMode ? 255 : 40),
color: LinkStyle.borderColor,
margin: EdgeInsets.symmetric(horizontal: 6),
),
FlowyIconButton(
icon: FlowySvg(FlowySvgs.toolbar_link_m),
tooltipText: LocaleKeys.editor_copyLink.tr(),
preferBelow: false,
width: 36,
height: 32,
onPressed: widget.onCopyLink,
onPressed: onCopyLink,
),
FlowyIconButton(
icon: FlowySvg(FlowySvgs.toolbar_link_edit_m),
tooltipText: LocaleKeys.editor_editLink.tr(),
preferBelow: false,
width: 36,
height: 32,
onPressed: widget.onEditLink,
onPressed: onEditLink,
),
buildConvertButton(),
FlowyIconButton(
icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m),
tooltipText: LocaleKeys.editor_removeLink.tr(),
preferBelow: false,
width: 36,
height: 32,
onPressed: widget.onRemoveLink,
onPressed: onRemoveLink,
),
],
),
@@ -422,13 +339,13 @@ class _LinkHoverMenuState extends State<LinkHoverMenu> {
),
MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: widget.onEnter,
onExit: widget.onExit,
onEnter: onEnter,
onExit: onExit,
child: GestureDetector(
onTap: widget.onOpenLink,
onTap: onOpenLink,
child: Container(
width: widget.triggerSize.width,
height: widget.triggerSize.height,
width: triggerSize.width,
height: triggerSize.height,
color: Colors.black.withAlpha(1),
),
),
@@ -436,113 +353,6 @@ class _LinkHoverMenuState extends State<LinkHoverMenu> {
],
);
}
Future<void> getPageView() async {
final viewId = href.split('/').lastOrNull ?? '';
final (view, isInTrash, isDeleted) =
await ViewBackendService.getMentionPageStatus(viewId);
if (mounted) {
setState(() {
currentView = view;
});
}
}
Widget buildLinkWidget() {
final view = currentView;
if (isPage && view == null) {
return SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(),
);
}
String text = '';
if (isPage && view != null) {
text = view.name;
if (text.isEmpty) {
text = LocaleKeys.document_title_placeholder.tr();
}
} else {
text = href;
}
return FlowyTooltip(
message: text,
preferBelow: false,
child: FlowyText.regular(
text,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 20,
fontSize: 14,
),
);
}
Widget buildConvertButton() {
return AppFlowyPopover(
offset: Offset(44, 10.0),
direction: PopoverDirection.bottomWithRightAligned,
margin: EdgeInsets.zero,
controller: popoverController,
onOpen: () => keepEditorFocusNotifier.increase(),
onClose: () => keepEditorFocusNotifier.decrease(),
popupBuilder: (context) => buildConvertMenu(),
child: FlowyIconButton(
icon: FlowySvg(FlowySvgs.turninto_m),
isSelected: isConvertButtonSelected,
tooltipText: LocaleKeys.editor_convertTo.tr(),
preferBelow: false,
width: 36,
height: 32,
onPressed: () {
setState(() {
isConvertButtonSelected = true;
});
showConvertMenu();
},
),
);
}
Widget buildConvertMenu() {
return MouseRegion(
onEnter: widget.onEnter,
onExit: widget.onExit,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SeparatedColumn(
mainAxisSize: MainAxisSize.min,
separatorBuilder: () => const VSpace(0.0),
children:
List.generate(LinkConvertMenuCommand.values.length, (index) {
final command = LinkConvertMenuCommand.values[index];
return SizedBox(
height: 36,
child: FlowyButton(
text: FlowyText(
command.title,
fontWeight: FontWeight.w400,
figmaLineHeight: 20,
),
onTap: () {
widget.onConvertTo(command);
closeConvertMenu();
},
),
);
}),
),
),
);
}
void showConvertMenu() {
keepEditorFocusNotifier.increase();
popoverController.show();
}
void closeConvertMenu() {
popoverController.close();
}
}
class HoverTriggerKey {
@@ -557,11 +367,7 @@ class HoverTriggerKey {
other is HoverTriggerKey &&
runtimeType == other.runtimeType &&
nodeId == other.nodeId &&
isSelectionSame(other.selection);
bool isSelectionSame(Selection other) =>
(selection.start == other.start && selection.end == other.end) ||
(selection.start == other.end && selection.end == other.start);
selection == other.selection;
@override
int get hashCode => nodeId.hashCode ^ selection.hashCode;
@@ -584,52 +390,8 @@ class LinkHoverTriggers {
void call(HoverTriggerKey key) {
final callbacks = _map[key] ?? {};
if (callbacks.isEmpty) return;
callbacks.first.call();
}
}
enum LinkConvertMenuCommand {
toMention,
toBookmark,
toEmbed;
String get title {
switch (this) {
case toMention:
return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion
.tr();
case toBookmark:
return LocaleKeys
.document_plugins_linkPreview_linkPreviewMenu_toBookmark
.tr();
case toEmbed:
return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed
.tr();
}
}
String get type {
switch (this) {
case toMention:
return MentionBlockKeys.type;
case toBookmark:
return LinkPreviewBlockKeys.type;
case toEmbed:
return LinkPreviewBlockKeys.type;
}
}
}
extension LinkExtension on BuildContext {
Future<void> copyLink(String link) async {
if (link.isEmpty) return;
await getIt<ClipboardService>()
.setData(ClipboardServiceData(plainText: link));
if (mounted) {
showToastNotification(
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
);
for (final callback in callbacks) {
callback.call();
}
}
}
@@ -1,184 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// ignore: implementation_imports
import 'package:appflowy_editor/src/editor/util/link_util.dart';
import 'package:flutter/services.dart';
import 'link_create_menu.dart';
import 'link_styles.dart';
void showReplaceMenu({
required BuildContext context,
required EditorState editorState,
required Node node,
String? url,
required LTRB ltrb,
required ValueChanged<String> onReplace,
}) {
OverlayEntry? overlay;
void dismissOverlay() {
keepEditorFocusNotifier.decrease();
overlay?.remove();
overlay = null;
}
keepEditorFocusNotifier.increase();
overlay = FullScreenOverlayEntry(
top: ltrb.top,
bottom: ltrb.bottom,
left: ltrb.left,
right: ltrb.right,
dismissCallback: () => keepEditorFocusNotifier.decrease(),
builder: (context) {
return LinkReplaceMenu(
link: url ?? '',
onSubmitted: (link) async {
onReplace.call(link);
dismissOverlay();
},
onDismiss: dismissOverlay,
);
},
).build();
Overlay.of(context, rootOverlay: true).insert(overlay!);
}
class LinkReplaceMenu extends StatefulWidget {
const LinkReplaceMenu({
super.key,
required this.onSubmitted,
required this.link,
required this.onDismiss,
});
final ValueChanged<String> onSubmitted;
final VoidCallback onDismiss;
final String link;
@override
State<LinkReplaceMenu> createState() => _LinkReplaceMenuState();
}
class _LinkReplaceMenuState extends State<LinkReplaceMenu> {
bool showErrorText = false;
late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent);
late TextEditingController textEditingController =
TextEditingController(text: widget.link);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
@override
void dispose() {
focusNode.dispose();
textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: 330,
padding: EdgeInsets.all(8),
decoration: buildToolbarLinkDecoration(context),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: buildLinkField()),
HSpace(8),
buildReplaceButton(),
],
),
);
}
Widget buildLinkField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 32,
child: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
autofocus: true,
focusNode: focusNode,
textAlign: TextAlign.left,
controller: textEditingController,
style: TextStyle(
fontSize: 14,
height: 20 / 14,
fontWeight: FontWeight.w400,
),
decoration: LinkStyle.buildLinkTextFieldInputDecoration(
LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_pasteHint
.tr(),
context,
showErrorBorder: showErrorText,
),
),
),
if (showErrorText)
Padding(
padding: const EdgeInsets.only(top: 4),
child: FlowyText.regular(
LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),
color: LinkStyle.textStatusError,
fontSize: 12,
figmaLineHeight: 16,
),
),
],
);
}
Widget buildReplaceButton() {
return FlowyTextButton(
LocaleKeys.button_replace.tr(),
padding: EdgeInsets.zero,
mainAxisAlignment: MainAxisAlignment.center,
constraints: BoxConstraints(maxWidth: 78, minHeight: 32),
fontSize: 14,
lineHeight: 20 / 14,
hoverColor: LinkStyle.fillThemeThick.withAlpha(200),
fontColor: Colors.white,
fillColor: LinkStyle.fillThemeThick,
fontWeight: FontWeight.w400,
onPressed: onSubmit,
);
}
void onSubmit() {
final link = textEditingController.text.trim();
if (link.isEmpty || !isUri(link)) {
setState(() {
showErrorText = true;
});
return;
}
widget.onSubmitted.call(link);
}
KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) {
if (key is! KeyDownEvent) return KeyEventResult.ignored;
if (key.logicalKey == LogicalKeyboardKey.escape) {
widget.onDismiss.call();
return KeyEventResult.handled;
} else if (key.logicalKey == LogicalKeyboardKey.enter) {
onSubmit();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
}
@@ -12,8 +12,6 @@ import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
// ignore: implementation_imports
import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
// ignore: implementation_imports
import 'package:appflowy_editor/src/editor/util/link_util.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -27,16 +25,12 @@ class LinkSearchTextField {
this.onEscape,
this.onEnter,
this.onDataRefresh,
this.initialViewId = '',
required this.currentViewId,
String? initialSearchText,
}) : textEditingController = TextEditingController(
text: isUri(initialSearchText ?? '') ? initialSearchText : '',
text: initialSearchText ?? '',
);
final TextEditingController textEditingController;
final String initialViewId;
final String currentViewId;
final ItemScrollController searchController = ItemScrollController();
late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent);
final List<ViewPB> searchedViews = [];
@@ -49,7 +43,7 @@ class LinkSearchTextField {
String get searchText => textEditingController.text;
bool get isTextfieldEnable => searchText.isNotEmpty && isUri(searchText);
bool get isButtonEnable => searchText.isNotEmpty;
bool get showingRecent => searchText.isEmpty && recentViews.isNotEmpty;
@@ -64,11 +58,7 @@ class LinkSearchTextField {
recentViews.clear();
}
Widget buildTextField({
bool autofocus = false,
bool showError = false,
required BuildContext context,
}) {
Widget buildTextField({bool autofocus = false}) {
return TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
autofocus: autofocus,
@@ -91,8 +81,6 @@ class LinkSearchTextField {
},
decoration: LinkStyle.buildLinkTextFieldInputDecoration(
LocaleKeys.document_toolbar_linkInputHint.tr(),
context,
showErrorBorder: showError,
),
);
}
@@ -189,41 +177,29 @@ class LinkSearchTextField {
bool isSelected,
ValueChanged<ViewPB>? onSubmittedPageLink,
) {
final viewName = view.name;
final displayName = viewName.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: viewName;
final isCurrent = initialViewId == view.id;
return SizedBox(
height: 32,
child: FlowyButton(
isSelected: isSelected,
leftIcon: buildIcon(view, padding: EdgeInsets.zero),
leftIcon: buildIcon(view),
text: FlowyText.regular(
displayName,
view.name,
overflow: TextOverflow.ellipsis,
fontSize: 14,
figmaLineHeight: 20,
),
rightIcon: isCurrent ? FlowySvg(FlowySvgs.toolbar_check_m) : null,
onTap: () => onSubmittedPageLink?.call(view),
),
);
}
Widget buildIcon(
ViewPB view, {
EdgeInsetsGeometry padding = const EdgeInsets.only(top: 4),
}) {
Widget buildIcon(ViewPB view) {
if (view.icon.value.isEmpty) return view.defaultIcon(size: Size(20, 20));
final iconData = view.icon.toEmojiIconData();
return Padding(
padding: padding,
child: RawEmojiIconWidget(
emoji: iconData,
emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20,
lineHeight: 1,
),
return RawEmojiIconWidget(
emoji: iconData,
emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20,
lineHeight: 1,
);
}
@@ -312,7 +288,6 @@ class LinkSearchTextField {
final views = sectionViews
.unique((e) => e.item.id)
.map((e) => e.item)
.where((e) => e.id != currentViewId)
.take(5)
.toList();
recentViews.clear();
@@ -328,14 +303,13 @@ class LinkSearchTextField {
?.items
.where(
(view) =>
(view.id != currentViewId) &&
(view.name.toLowerCase().contains(search.toLowerCase()) ||
(view.name.isEmpty && search.isEmpty) ||
(view.name.isEmpty &&
LocaleKeys.menuAppHeader_defaultNewPageName
.tr()
.toLowerCase()
.contains(search.toLowerCase()))),
view.name.toLowerCase().contains(search.toLowerCase()) ||
(view.name.isEmpty && search.isEmpty) ||
(view.name.isEmpty &&
LocaleKeys.menuAppHeader_defaultNewPageName
.tr()
.toLowerCase()
.contains(search.toLowerCase())),
)
.take(10)
.toList();
@@ -1,31 +1,19 @@
import 'package:appflowy/util/theme_extension.dart';
import 'package:flutter/material.dart';
class LinkStyle {
static const borderColor = Color(0xFFE8ECF3);
static const textTertiary = Color(0xFF99A1A8);
static const textStatusError = Color(0xffE71D32);
static const fillThemeThick = Color(0xFF00B5FF);
static const shadowMedium = Color(0x1F22251F);
static const textPrimary = Color(0xFF1F2329);
static Color borderColor(BuildContext context) =>
Theme.of(context).isLightMode ? Color(0xFFE8ECF3) : Color(0x64BDBDBD);
static InputDecoration buildLinkTextFieldInputDecoration(
String hintText,
BuildContext context, {
bool showErrorBorder = false,
}) {
final border = OutlineInputBorder(
static InputDecoration buildLinkTextFieldInputDecoration(String hintText) {
const border = OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)),
borderSide: BorderSide(color: borderColor(context)),
borderSide: BorderSide(color: LinkStyle.borderColor),
);
final enableBorder = border.copyWith(
borderSide: BorderSide(
color: showErrorBorder
? LinkStyle.textStatusError
: LinkStyle.fillThemeThick,
),
borderSide: BorderSide(color: LinkStyle.fillThemeThick),
);
const hintStyle = TextStyle(
fontSize: 14,
@@ -0,0 +1,17 @@
import 'dart:ui';
import 'package:bloc/bloc.dart';
class ToolbarCubit extends Cubit<ToolbarState> {
ToolbarCubit(this.onDismissCallback) : super(ToolbarState._());
final VoidCallback onDismissCallback;
void dismiss() {
onDismissCallback.call();
}
}
class ToolbarState {
const ToolbarState._();
}
@@ -154,6 +154,7 @@ class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
void _copyBlockContent() {
showToastNotification(
context,
message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),
);
@@ -104,10 +104,10 @@ Future<void> downloadMediaFile(
await afLaunchUrlString(file.url);
} else {
if (userProfile == null) {
showToastNotification(
return showToastNotification(
context,
message: LocaleKeys.grid_media_downloadFailedToken.tr(),
);
return;
}
final uri = Uri.parse(file.url);
@@ -128,12 +128,14 @@ Future<void> downloadMediaFile(
if (result != null && context.mounted) {
showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys.grid_media_downloadSuccess.tr(),
);
}
} else if (context.mounted) {
showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
);
@@ -157,11 +159,13 @@ Future<void> downloadMediaFile(
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.grid_media_downloadSuccess.tr(),
);
}
} else if (context.mounted) {
showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
);
@@ -8,7 +8,6 @@ import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/material.dart';
import 'package:string_validator/string_validator.dart';
@@ -114,21 +113,24 @@ class _RawEmojiIconWidgetState extends State<RawEmojiIconWidget> {
try {
switch (widget.emoji.type) {
case FlowyIconType.emoji:
return FlowyText.emoji(
widget.emoji.emoji,
return EmojiText(
emoji: widget.emoji.emoji,
fontSize: widget.emojiSize,
textAlign: TextAlign.justify,
lineHeight: widget.lineHeight,
);
case FlowyIconType.icon:
IconsData iconData = IconsData.fromJson(
jsonDecode(widget.emoji.emoji),
);
IconsData iconData =
IconsData.fromJson(jsonDecode(widget.emoji.emoji));
if (!widget.enableColor) {
iconData = iconData.noColor();
}
final iconSize = widget.emojiSize;
/// 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 ? widget.emojiSize * 0.9 : widget.emojiSize;
return IconWidget(
iconsData: iconData,
size: iconSize,
@@ -378,6 +378,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
onTap: () async {
context.pop();
showToastNotification(
context,
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
);
await getIt<ClipboardService>().setPlainText(url);
@@ -430,6 +431,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
);
if (mounted) {
showToastNotification(
context,
message: result.isSuccess
? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr()
: LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(),
@@ -117,12 +117,14 @@ class _ImageMenuState extends State<ImageMenu> {
if (mounted) {
showToastNotification(
context,
message: LocaleKeys.message_copy_success.tr(),
);
}
} catch (e) {
if (mounted) {
showToastNotification(
context,
message: LocaleKeys.message_copy_fail.tr(),
type: ToastificationType.error,
);
@@ -218,6 +218,7 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
ClipboardData(text: images[widget.indexNotifier.value].url),
);
showToastNotification(
context,
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
);
}
Loaded 100 of 520 files, more files were not shown because too many files have changed in this diff. Show more