From f4af47728fb59241a8c490db64f5f0be6b479b4f Mon Sep 17 00:00:00 2001 From: Morn Date: Wed, 9 Apr 2025 12:57:58 +0800 Subject: [PATCH 1/7] fix: overwriting a selected text removes more than expected (#7714) --- .../document/document_selection_test.dart | 37 +++++++++++++++++++ .../plugins/emoji/emoji_actions_command.dart | 6 +-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart index bd0fd18c50..de1cb880a5 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart @@ -1,5 +1,6 @@ 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'; @@ -47,5 +48,41 @@ 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)); + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart index ffe25be8bd..9d386b36be 100644 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart @@ -38,10 +38,6 @@ Future emojiCommandHandler( return false; } - if (!selection.isCollapsed) { - await editorState.deleteSelection(selection); - } - final node = editorState.getNodeAtPath(selection.end.path); final delta = node?.delta; if (node == null || @@ -58,6 +54,8 @@ Future emojiCommandHandler( if (previousCharacter != _emojiCharacter) return false; if (!context.mounted) return false; + if (!selection.isCollapsed) return false; + await editorState.insertTextAtPosition( character, position: selection.start, From 8b82d532e019efa7a29261b6cd1f93846dd1e675 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 9 Apr 2025 13:21:19 +0800 Subject: [PATCH 2/7] fix: replace the selected text with ai response in the same line (#7708) * fix: replace the selected text with ai response in the same line * fix: replace the selected text with ai response in the multiple lines * fix: integrate the replace function in the ai writer cubit * fix: unit test and integration test --- .../ai/operations/ai_writer_cubit.dart | 62 ++- .../operations/ai_writer_node_extension.dart | 21 +- .../base/markdown_text_robot.dart | 188 ++++++- .../lib/shared/markdown_to_document.dart | 2 + frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../ai_writer_test/ai_writer_bloc_test.dart | 2 +- .../text_robot/markdown_text_robot_test.dart | 508 ++++++++++++++++++ 8 files changed, 773 insertions(+), 16 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 076046624a..7a885a3fe8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -15,6 +16,11 @@ 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 = false; + class AiWriterCubit extends Cubit { AiWriterCubit({ required this.documentId, @@ -95,6 +101,10 @@ class AiWriterCubit extends Cubit { final command = node.aiWriterCommand; final (run, prompt) = await _addSelectionTextToRecords(command); + _aiWriterCubitLog( + 'command: $command, run: $run, prompt: $prompt', + ); + if (!run) { await exit(); return; @@ -211,20 +221,26 @@ class AiWriterCubit extends Cubit { return; } + // Accept + // + // If the user clicks accept, we need to replace the selection with the AI's response if (action case SuggestionAction.accept) { - await _textRobot.persist(); - await formatSelection( - editorState, - selection, - ApplySuggestionFormatType.clear, + // trim the markdown text to avoid extra new lines + final trimmedMarkdownText = _textRobot.markdownText.trim(); + + _aiWriterCubitLog( + 'trigger accept action, markdown text: $trimmedMarkdownText', ); - final nodes = editorState.getNodesInSelection(selection); - final transaction = editorState.transaction..deleteNodes(nodes); - await editorState.apply( - transaction, - withUpdateSelection: false, + + await _textRobot.deleteAINodes(); + + await _textRobot.replace( + selection: selection, + markdownText: trimmedMarkdownText, ); + await exit(withDiscard: false, withUnformat: false); + return; } @@ -276,17 +292,24 @@ class AiWriterCubit extends Cubit { AiWriterCommand command, ) async { final node = aiWriterNode; + + // check the node is registered if (node == null) { return (false, ''); } + + // check the selection is valid final selection = node.aiWriterSelection?.normalized; if (selection == 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, ''); } @@ -297,6 +320,7 @@ class AiWriterCubit extends Cubit { records.add( AiWriterRecord.user(content: selectionText, format: null), ); + return (true, ''); } else { return (true, selectionText); @@ -540,6 +564,10 @@ class AiWriterCubit extends Cubit { attributes: ApplySuggestionFormatType.replace.attributes, ); onAppendToDocument?.call(); + + _aiWriterCubitLog( + 'received message: $text', + ); }, processAssistMessage: (text) async { if (state case final GeneratingAiWriterState generatingState) { @@ -551,6 +579,10 @@ class AiWriterCubit extends Cubit { ), ); } + + _aiWriterCubitLog( + 'received assist message: $text', + ); }, onEnd: () async { if (state case final GeneratingAiWriterState generatingState) { @@ -567,6 +599,10 @@ class AiWriterCubit extends Cubit { records.add( AiWriterRecord.ai(content: _textRobot.markdownText), ); + + _aiWriterCubitLog( + 'returned response: ${_textRobot.markdownText}', + ); } }, onError: (error) async { @@ -658,6 +694,12 @@ class AiWriterCubit extends Cubit { ); } } + + void _aiWriterCubitLog(String message) { + if (_aiWriterCubitDebugLog) { + Log.debug('[AiWriterCubit] $message'); + } + } } mixin RegisteredAiWriter { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart index 168854a34d..6f4456f9ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart @@ -57,11 +57,30 @@ extension AiWriterNodeExtension on EditorState { slicedNodes.add(copiedNode); } + for (final (i, node) in slicedNodes.indexed) { + final childNodesShouldBeDeleted = []; + 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\n', ); - return markdown; + // trim the last \n if it exists + return markdown.trimRight(); } List getPlainTextInSelection(Selection? selection) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index d6cfee929c..031e157b33 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -110,10 +110,13 @@ class MarkdownTextRobot { } /// Persist the text into the document - Future persist({String? markdownText}) async { + Future persist({ + String? markdownText, + }) async { if (markdownText != null) { _markdownText = markdownText; } + await _lock.synchronized(() async { await _refresh(inMemoryUpdate: false); }); @@ -124,6 +127,34 @@ class MarkdownTextRobot { } } + /// Replace the selected content with the AI's response + Future 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 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 discard() async { final start = _insertPosition; @@ -282,6 +313,161 @@ class MarkdownTextRobot { children: children, ); } + + /// If the selected content is in the same line, + /// keep the selected node and replace the delta. + Future _replaceInSameLine({ + required Selection selection, + required String markdownText, + }) async { + 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 decoder = DeltaMarkdownDecoder(); + final markdownDelta = + document.nodeAtPath([0])?.delta ?? decoder.convert(markdownText); + + // Replace the delta of the selected node. + final transaction = editorState.transaction; + transaction + ..deleteText(node, startIndex, length) + ..insertTextDelta(node, startIndex, markdownDelta); + await editorState.apply(transaction); + } + + /// If the selected content is in multiple lines + Future _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 nodes = editorState.getNodesInSelection(selection); + + // 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); + } + + // step 2 + 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) { + 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); + } + } + + // step 3 + final insertedPath = selection.start.path.nextNPath(1); + if (markdownNodes.length > 2) { + transaction.insertNodes( + insertedPath, + markdownNodes.skip(1).take(markdownNodes.length - 2).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 { diff --git a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart index 4be6fdbe11..912f96bd05 100644 --- a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart +++ b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart @@ -27,6 +27,7 @@ Future customDocumentToMarkdown( Document document, { String path = '', AsyncValueSetter? onArchive, + String lineBreak = '', }) async { final List> fileFutures = []; @@ -41,6 +42,7 @@ Future customDocumentToMarkdown( try { markdown = documentToMarkdown( document, + lineBreak: lineBreak, customParsers: [ const MathEquationNodeParser(), const CalloutNodeParser(), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 083f945636..fecbc32b89 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -98,8 +98,8 @@ packages: dependency: "direct main" description: path: "." - ref: "552f95f" - resolved-ref: "552f95fd15627e10a138c6db2a6d0a8089bc9a25" + ref: "361b99c38370abeeb19656f89e8c31cb3666623b" + resolved-ref: "361b99c38370abeeb19656f89e8c31cb3666623b" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index e6e377fa24..0dc9ec673c 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -184,7 +184,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "552f95f" + ref: "361b99c38370abeeb19656f89e8c31cb3666623b" appflowy_editor_plugins: git: diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index bcd8b13d39..46b8118087 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -375,7 +375,7 @@ void main() { await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.accept); await blocResponseFuture(); - expect(editorState.document.root.children.length, 1); + expect(editorState.document.root.children.length, 2); expect( editorState.getNodeAtPath([0])!.delta!.toPlainText(), 'Hello World', diff --git a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart index 0b6289c784..f2fcf8cd65 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart @@ -294,6 +294,514 @@ void main() { ); }); }); + + group('markdown text robot - replace in same line:', () { + final text1 = + '''The introduction of the World Wide Web in the early 1990s marked a turning point. '''; + final text2 = + '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. '''; + final text3 = + '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; + + Document buildTestDocument() { + return Document( + root: pageNode( + children: [ + paragraphNode(delta: Delta()..insert(text1 + text2 + text3)), + ], + ), + ); + } + + // 1. create a document with a paragraph node + // 2. use the text robot to replace the selected content in the same line + // 3. check the document + test('the selection is in the middle of the text', () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + offset: text1.length, + ), + end: Position( + path: [0], + offset: text1.length + text2.length, + ), + ); + + final markdownText = + '''Tim Berners-Lee's invention of the **World Wide Web** transformed the internet, making it accessible to _non-technical users_ and opening the floodgates for global mass adoption.'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterDelta = editorState.document.root.children[0].delta!.toList(); + expect(afterDelta.length, 5); + + final d1 = afterDelta[0] as TextInsert; + expect(d1.text, '${text1}Tim Berners-Lee\'s invention of the '); + expect(d1.attributes, null); + + final d2 = afterDelta[1] as TextInsert; + expect(d2.text, 'World Wide Web'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = afterDelta[2] as TextInsert; + expect(d3.text, ' transformed the internet, making it accessible to '); + expect(d3.attributes, null); + + final d4 = afterDelta[3] as TextInsert; + expect(d4.text, 'non-technical users'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = afterDelta[4] as TextInsert; + expect( + d5.text, + ' and opening the floodgates for global mass adoption.$text3', + ); + expect(d5.attributes, null); + }); + + test('replace markdown text with selection from start to middle', () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + ), + end: Position( + path: [0], + offset: text1.length, + ), + ); + + final markdownText = + '''The **invention** of the _World Wide Web_ by Tim Berners-Lee transformed how we access information.'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterDelta = editorState.document.root.children[0].delta!.toList(); + expect(afterDelta.length, 5); + + final d1 = afterDelta[0] as TextInsert; + expect(d1.text, 'The '); + expect(d1.attributes, null); + + final d2 = afterDelta[1] as TextInsert; + expect(d2.text, 'invention'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = afterDelta[2] as TextInsert; + expect(d3.text, ' of the '); + expect(d3.attributes, null); + + final d4 = afterDelta[3] as TextInsert; + expect(d4.text, 'World Wide Web'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = afterDelta[4] as TextInsert; + expect( + d5.text, + ' by Tim Berners-Lee transformed how we access information.$text2$text3', + ); + expect(d5.attributes, null); + }); + + test('replace markdown text with selection from middle to end', () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + offset: text1.length + text2.length, + ), + end: Position( + path: [0], + offset: text1.length + text2.length + text3.length, + ), + ); + + final markdownText = + '''**Email** became widespread, and instant messaging services like *ICQ* and **AOL Instant Messenger** gained tremendous popularity, allowing for seamless real-time text communication across the globe.'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterDelta = editorState.document.root.children[0].delta!.toList(); + expect(afterDelta.length, 7); + + final d1 = afterDelta[0] as TextInsert; + expect( + d1.text, + text1 + text2, + ); + expect(d1.attributes, null); + + final d2 = afterDelta[1] as TextInsert; + expect(d2.text, 'Email'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = afterDelta[2] as TextInsert; + expect( + d3.text, + ' became widespread, and instant messaging services like ', + ); + expect(d3.attributes, null); + + final d4 = afterDelta[3] as TextInsert; + expect(d4.text, 'ICQ'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = afterDelta[4] as TextInsert; + expect(d5.text, ' and '); + expect(d5.attributes, null); + + final d6 = afterDelta[5] as TextInsert; + expect( + d6.text, + 'AOL Instant Messenger', + ); + expect(d6.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d7 = afterDelta[6] as TextInsert; + expect( + d7.text, + ' gained tremendous popularity, allowing for seamless real-time text communication across the globe.', + ); + expect(d7.attributes, null); + }); + }); + + group('markdown text robot - replace in multiple lines:', () { + final text1 = + '''The introduction of the World Wide Web in the early 1990s marked a turning point. '''; + final text2 = + '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. '''; + final text3 = + '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; + + Document buildTestDocument() { + return Document( + root: pageNode( + children: [ + paragraphNode(delta: Delta()..insert(text1)), + paragraphNode(delta: Delta()..insert(text2)), + paragraphNode(delta: Delta()..insert(text3)), + ], + ), + ); + } + + // 1. create a document with 3 paragraph nodes + // 2. use the text robot to replace the selected content in the multiple lines + // 3. check the document + test( + 'the selection starts with the first paragraph and ends with the middle of second paragraph', + () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + ), + end: Position( + path: [1], + offset: text2.length - + ', opening the floodgates for mass adoption. '.length, + ), + ); + + final markdownText = + '''The **introduction** of the World Wide Web in the *early 1990s* marked a significant turning point. + +Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterNodes = editorState.document.root.children; + expect(afterNodes.length, 3); + + { + // first paragraph + final delta1 = afterNodes[0].delta!.toList(); + expect(delta1.length, 5); + + final d1 = delta1[0] as TextInsert; + expect(d1.text, 'The '); + expect(d1.attributes, null); + + final d2 = delta1[1] as TextInsert; + expect(d2.text, 'introduction'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta1[2] as TextInsert; + expect(d3.text, ' of the World Wide Web in the '); + expect(d3.attributes, null); + + final d4 = delta1[3] as TextInsert; + expect(d4.text, 'early 1990s'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta1[4] as TextInsert; + expect(d5.text, ' marked a significant turning point.'); + expect(d5.attributes, null); + } + + { + // second paragraph + final delta2 = afterNodes[1].delta!.toList(); + expect(delta2.length, 3); + + final d1 = delta2[0] as TextInsert; + expect(d1.text, "Tim Berners-Lee's "); + expect(d1.attributes, null); + + final d2 = delta2[1] as TextInsert; + expect(d2.text, "revolutionary invention"); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta2[2] as TextInsert; + expect( + d3.text, + " made the internet accessible to non-technical users, opening the floodgates for mass adoption. ", + ); + expect(d3.attributes, null); + } + + { + // third paragraph + final delta3 = afterNodes[2].delta!.toList(); + expect(delta3.length, 1); + + final d1 = delta3[0] as TextInsert; + expect(d1.text, text3); + expect(d1.attributes, null); + } + }); + + test( + 'the selection starts with the middle of the first paragraph and ends with the middle of last paragraph', + () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + offset: 'The introduction of the World Wide Web'.length, + ), + end: Position( + path: [2], + offset: + 'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity' + .length, + ), + ); + + final markdownText = + ''' in the **early 1990s** marked a *significant turning point* in technological history. + +Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users, opening the floodgates for *unprecedented mass adoption*. + +Email became **widely prevalent**, and instant messaging services like *ICQ* and *AOL Instant Messenger* gained tremendous popularity + '''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterNodes = editorState.document.root.children; + expect(afterNodes.length, 3); + + { + // first paragraph + final delta1 = afterNodes[0].delta!.toList(); + expect(delta1.length, 5); + + final d1 = delta1[0] as TextInsert; + expect(d1.text, 'The introduction of the World Wide Web in the '); + expect(d1.attributes, null); + + final d2 = delta1[1] as TextInsert; + expect(d2.text, 'early 1990s'); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta1[2] as TextInsert; + expect(d3.text, ' marked a '); + expect(d3.attributes, null); + + final d4 = delta1[3] as TextInsert; + expect(d4.text, 'significant turning point'); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta1[4] as TextInsert; + expect(d5.text, ' in technological history.'); + expect(d5.attributes, null); + } + + { + // second paragraph + final delta2 = afterNodes[1].delta!.toList(); + expect(delta2.length, 5); + + final d1 = delta2[0] as TextInsert; + expect(d1.text, "Tim Berners-Lee's "); + expect(d1.attributes, null); + + final d2 = delta2[1] as TextInsert; + expect(d2.text, "revolutionary invention"); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta2[2] as TextInsert; + expect( + d3.text, + " made the internet accessible to non-technical users, opening the floodgates for ", + ); + expect(d3.attributes, null); + + final d4 = delta2[3] as TextInsert; + expect(d4.text, "unprecedented mass adoption"); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta2[4] as TextInsert; + expect(d5.text, "."); + expect(d5.attributes, null); + } + + { + // third paragraph + // third paragraph + final delta3 = afterNodes[2].delta!.toList(); + expect(delta3.length, 7); + + final d1 = delta3[0] as TextInsert; + expect(d1.text, "Email became "); + expect(d1.attributes, null); + + final d2 = delta3[1] as TextInsert; + expect(d2.text, "widely prevalent"); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta3[2] as TextInsert; + expect(d3.text, ", and instant messaging services like "); + expect(d3.attributes, null); + + final d4 = delta3[3] as TextInsert; + expect(d4.text, "ICQ"); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta3[4] as TextInsert; + expect(d5.text, " and "); + expect(d5.attributes, null); + + final d6 = delta3[5] as TextInsert; + expect(d6.text, "AOL Instant Messenger"); + expect(d6.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d7 = delta3[6] as TextInsert; + expect( + d7.text, + " gained tremendous popularity, allowing for real-time text communication.", + ); + expect(d7.attributes, null); + } + }); + + test( + 'the length of the returned response less than the length of the selected text', + () async { + final document = buildTestDocument(); + final editorState = EditorState(document: document); + + editorState.selection = Selection( + start: Position( + path: [0], + offset: 'The introduction of the World Wide Web'.length, + ), + end: Position( + path: [2], + offset: + 'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity' + .length, + ), + ); + + final markdownText = + ''' in the **early 1990s** marked a *significant turning point* in technological history.'''; + final markdownTextRobot = MarkdownTextRobot( + editorState: editorState, + ); + await markdownTextRobot.replace( + selection: editorState.selection!, + markdownText: markdownText, + ); + + final afterNodes = editorState.document.root.children; + expect(afterNodes.length, 2); + + { + // first paragraph + final delta1 = afterNodes[0].delta!.toList(); + expect(delta1.length, 5); + + final d1 = delta1[0] as TextInsert; + expect(d1.text, "The introduction of the World Wide Web in the "); + expect(d1.attributes, null); + + final d2 = delta1[1] as TextInsert; + expect(d2.text, "early 1990s"); + expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); + + final d3 = delta1[2] as TextInsert; + expect(d3.text, " marked a "); + expect(d3.attributes, null); + + final d4 = delta1[3] as TextInsert; + expect(d4.text, "significant turning point"); + expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); + + final d5 = delta1[4] as TextInsert; + expect(d5.text, " in technological history."); + expect(d5.attributes, null); + } + + { + // second paragraph + final delta2 = afterNodes[1].delta!.toList(); + expect(delta2.length, 1); + + final d1 = delta2[0] as TextInsert; + expect(d1.text, ", allowing for real-time text communication."); + expect(d1.attributes, null); + } + }); + }); } const _sample1 = '''# The Curious Cat From ad227bcf79b973fcc994d3d4c0fa507e823dd088 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:45:07 +0800 Subject: [PATCH 3/7] chore: toast message when theme folder doesn't have permission (#7715) --- .../lib/core/helpers/url_launcher.dart | 1 - .../presentation/base/mobile_view_page.dart | 2 -- .../base/view_page/more_bottom_sheet.dart | 11 ----------- .../bottom_sheet/bottom_sheet_view_item.dart | 3 --- .../default_mobile_action_pane.dart | 2 -- .../card_detail/widgets/row_page_button.dart | 2 +- .../presentation/home/mobile_home_page.dart | 2 +- .../home/space/mobile_space_menu.dart | 4 ---- .../mobile_bottom_navigation_bar.dart | 2 -- .../widgets/settings_popup_menu.dart | 3 --- .../notifications/widgets/slide_actions.dart | 4 ---- .../presentation/notifications/widgets/tab.dart | 1 - .../setting/support_setting_group.dart | 1 - .../workspace/invite_members_screen.dart | 7 ------- .../message/ai_message_action_bar.dart | 1 - .../presentation/message/ai_message_bubble.dart | 1 - .../presentation/message/message_util.dart | 2 -- .../document/application/document_bloc.dart | 1 - .../ai/ai_writer_toolbar_item.dart | 2 -- .../code_block/code_block_copy_button.dart | 1 - .../copy_and_paste/paste_from_image.dart | 3 --- .../desktop_toolbar/link/link_hover_menu.dart | 1 - .../error/error_block_component_builder.dart | 1 - .../editor_plugins/file/file_util.dart | 5 ----- .../custom_image_block_component.dart | 2 -- .../image_menu.dart | 2 -- .../multi_image_menu.dart | 1 - .../link_preview/link_preview_menu.dart | 1 - .../mention/child_page_transaction_handler.dart | 1 - .../lib/plugins/shared/share/export_tab.dart | 3 +-- .../lib/plugins/shared/share/publish_tab.dart | 9 --------- .../lib/plugins/shared/share/share_button.dart | 2 -- .../lib/plugins/shared/share/share_tab.dart | 1 - .../lib/shared/flowy_error_page.dart | 3 +-- .../lib/startup/tasks/appflowy_cloud_task.dart | 2 +- .../helpers/handle_open_workspace_error.dart | 4 ++-- .../widgets/magic_link_sign_in_buttons.dart | 2 +- .../lib/util/share_log_files.dart | 6 +++--- .../settings/appearance/appearance_cubit.dart | 17 ++++++++++++++++- .../sidebar/workspace/sidebar_workspace.dart | 2 +- .../pages/account/account_deletion.dart | 8 ++++---- .../pages/settings_manage_data_view.dart | 2 +- .../settings/pages/sites/constants.dart | 1 - .../pages/sites/domain/domain_item.dart | 2 -- .../sites/domain/domain_settings_dialog.dart | 2 -- .../published_view_settings_dialog.dart | 2 -- .../pages/sites/settings_sites_view.dart | 7 ------- .../presentation/settings/settings_dialog.dart | 4 ---- .../workspace/presentation/widgets/dialogs.dart | 4 +--- .../widgets/float_bubble/version_section.dart | 1 - .../presentation/widgets/view_title_bar.dart | 1 - frontend/resources/translations/en.json | 3 ++- 52 files changed, 36 insertions(+), 122 deletions(-) diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart index a27ab07e9d..fa61e7f5b8 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -133,7 +133,6 @@ Future _afLaunchLocalUri( }; if (context != null && context.mounted) { showToastNotification( - context, message: message, type: result.type == ResultType.done ? ToastificationType.success diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 792679daf1..318b06394a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -336,7 +336,6 @@ class _MobileViewPageState extends State { listener: (context, state) { if (state.isLocked) { showToastNotification( - context, message: LocaleKeys.lockPage_pageLockedToast.tr(), ); @@ -366,7 +365,6 @@ class _MobileViewPageState extends State { listener: (context, state) { if (state.isLocked) { showToastNotification( - context, message: LocaleKeys.lockPage_pageLockedToast.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart index 080d83e83c..6609b10136 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -161,7 +161,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { context.pop(); showToastNotification( - context, message: LocaleKeys.button_duplicateSuccessfully.tr(), ); } @@ -170,7 +169,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { _toggleFavorite(context); showToastNotification( - context, message: LocaleKeys.button_favoriteSuccessfully.tr(), ); } @@ -179,7 +177,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { _toggleFavorite(context); showToastNotification( - context, message: LocaleKeys.button_unfavoriteSuccessfully.tr(), ); } @@ -202,7 +199,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { ), ); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } @@ -234,12 +230,10 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { ), ); showToastNotification( - context, message: LocaleKeys.shareAction_copyLinkSuccess.tr(), ); } else { showToastNotification( - context, message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(), type: ToastificationType.error, ); @@ -323,11 +317,9 @@ 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, ), @@ -335,11 +327,9 @@ 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, @@ -349,7 +339,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { state.updatePathNameResult!.onSuccess( (value) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart index c1129af79d..86021ea938 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart @@ -65,7 +65,6 @@ class _MobileViewItemBottomSheetState extends State { Navigator.pop(context); context.read().add(const ViewEvent.duplicate()); showToastNotification( - context, message: LocaleKeys.button_duplicateSuccessfully.tr(), ); break; @@ -84,7 +83,6 @@ class _MobileViewItemBottomSheetState extends State { .read() .add(FavoriteEvent.toggle(widget.view)); showToastNotification( - context, message: !widget.view.isFavorite ? LocaleKeys.button_favoriteSuccessfully.tr() : LocaleKeys.button_unfavoriteSuccessfully.tr(), @@ -146,7 +144,6 @@ class _MobileViewItemBottomSheetState extends State { Navigator.pop(context); showToastNotification( - context, message: LocaleKeys.sideBar_removeSuccess.tr(), ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart index cb840b0f40..d4b4292443 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -45,7 +45,6 @@ enum MobilePaneActionType { size: 24.0, onPressed: (context) { showToastNotification( - context, message: LocaleKeys.button_unfavoriteSuccessfully.tr(), ); @@ -61,7 +60,6 @@ enum MobilePaneActionType { size: 24.0, onPressed: (context) { showToastNotification( - context, message: LocaleKeys.button_favoriteSuccessfully.tr(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart index fa3494002d..b0f21188cd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart @@ -103,7 +103,7 @@ class _OpenRowPageButtonState extends State { Log.info('Open row page(${widget.documentId})'); if (view == null) { - showToastNotification(context, message: 'Failed to open row page'); + showToastNotification(message: 'Failed to open row page'); // reload the view again unawaited(_preloadView(context)); Log.error('Failed to open row page(${widget.documentId})'); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 2d409f58b6..1ae5d881ce 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -329,7 +329,7 @@ class _HomePageState extends State<_HomePage> { } if (message != null) { - showToastNotification(context, message: message, type: toastType); + showToastNotification(message: message, type: toastType); } } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart index 0197f34940..485e07a28c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart @@ -339,7 +339,6 @@ class _SpaceMenuItemTrailingState extends State { context.read().add(const SpaceEvent.duplicate()); showToastNotification( - context, message: LocaleKeys.space_success_duplicateSpace.tr(), ); @@ -374,7 +373,6 @@ class _SpaceMenuItemTrailingState extends State { .add(SpaceEvent.rename(space: widget.space, name: name)); showToastNotification( - context, message: LocaleKeys.space_success_renameSpace.tr(), ); }, @@ -424,7 +422,6 @@ class _SpaceMenuItemTrailingState extends State { ); showToastNotification( - context, message: LocaleKeys.space_success_updateSpace.tr(), ); @@ -457,7 +454,6 @@ class _SpaceMenuItemTrailingState extends State { context.read().add(SpaceEvent.delete(widget.space)); showToastNotification( - context, message: LocaleKeys.space_success_deleteSpace.tr(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart index 170ef46ac2..3c6adb8627 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -332,7 +332,6 @@ class _NotificationNavigationBar extends StatelessWidget { } showToastNotification( - context, message: LocaleKeys .settings_notifications_markAsReadNotifications_allSuccess .tr(), @@ -350,7 +349,6 @@ class _NotificationNavigationBar extends StatelessWidget { } showToastNotification( - context, message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess .tr(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart index dfa277f2ef..e694f9932d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart @@ -108,7 +108,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget { void _onMarkAllAsRead(BuildContext context) { showToastNotification( - context, message: LocaleKeys .settings_notifications_markAsReadNotifications_allSuccess .tr(), @@ -119,7 +118,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget { void _onArchiveAll(BuildContext context) { showToastNotification( - context, message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess .tr(), ); @@ -133,7 +131,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget { } showToastNotification( - context, message: 'Unarchive all success (Debug Mode)', ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart index 85f468c76c..d1216eed98 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart @@ -31,7 +31,6 @@ enum NotificationPaneActionType { size: 24.0, onPressed: (context) { showToastNotification( - context, message: LocaleKeys .settings_notifications_markAsReadNotifications_success .tr(), @@ -55,7 +54,6 @@ enum NotificationPaneActionType { size: 24.0, onPressed: (context) { showToastNotification( - context, message: 'Unarchive notification success', ); @@ -168,7 +166,6 @@ class _NotificationMoreActions extends StatelessWidget { Navigator.of(context).pop(); showToastNotification( - context, message: LocaleKeys.settings_notifications_markAsReadNotifications_success .tr(), ); @@ -191,7 +188,6 @@ class _NotificationMoreActions extends StatelessWidget { void _onArchive(BuildContext context) { showToastNotification( - context, message: LocaleKeys.settings_notifications_archiveNotifications_success .tr() .tr(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart index 7dda8f0a14..45e801e07c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart @@ -74,7 +74,6 @@ class _NotificationTabState extends State if (context.mounted) { showToastNotification( - context, message: LocaleKeys.settings_notifications_refreshSuccess.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart index 584b867736..e5e4efef77 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart @@ -81,7 +81,6 @@ class SupportSettingGroup extends StatelessWidget { ); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.settings_files_clearCacheSuccess.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart index 2e805c5c5a..4f16aabffd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart @@ -201,7 +201,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { result.fold( (s) { showToastNotification( - context, message: LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), bottomPadding: keyboardHeight, @@ -218,7 +217,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; }); showToastNotification( - context, type: ToastificationType.error, bottomPadding: keyboardHeight, message: message, @@ -229,7 +227,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { result.fold( (s) { showToastNotification( - context, message: LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), bottomPadding: keyboardHeight, @@ -247,7 +244,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; }); showToastNotification( - context, type: ToastificationType.error, message: message, bottomPadding: keyboardHeight, @@ -258,7 +254,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { result.fold( (s) { showToastNotification( - context, message: LocaleKeys .settings_appearance_members_removeFromWorkspaceSuccess .tr(), @@ -267,7 +262,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { }, (f) { showToastNotification( - context, type: ToastificationType.error, message: LocaleKeys .settings_appearance_members_removeFromWorkspaceFailed @@ -283,7 +277,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { final email = emailController.text; if (!isEmail(email)) { return showToastNotification( - context, type: ToastificationType.error, message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 945613490a..08fd82188d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -184,7 +184,6 @@ class CopyButton extends StatelessWidget { ); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart index 3ce80e2919..2786799520 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -376,7 +376,6 @@ class ChatAIMessagePopup extends StatelessWidget { } if (context.mounted) { showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart index 1b0084c77c..652fe3791b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart @@ -14,7 +14,6 @@ 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, ); @@ -36,7 +35,6 @@ void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) { return; } showToastNotification( - context, richMessage: TextSpan( children: [ TextSpan( diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 0a54ec5753..010dae1f12 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -442,7 +442,6 @@ class DocumentBloc extends Bloc { final context = AppGlobals.rootNavKey.currentContext; if (context != null && context.mounted) { showToastNotification( - context, message: 'document integrity check failed', type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 25502fd975..35575fe85a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -150,7 +150,6 @@ class _AiWriterToolbarActionListState extends State { }); } else { showToastNotification( - context, message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), ); } @@ -196,7 +195,6 @@ class ImproveWritingButton extends StatelessWidget { _insertAiNode(editorState, AiWriterCommand.improveWriting); } else { showToastNotification( - context, message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart index cc80119cb9..645de3b2f8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart @@ -47,7 +47,6 @@ class _CopyButton extends StatelessWidget { if (context.mounted) { showToastNotification( - context, message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart index 6e6c9b1772..d086f36bed 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart @@ -73,7 +73,6 @@ extension PasteFromImage on EditorState { Log.info('unsupported format: $format'); if (UniversalPlatform.isMobile) { showToastNotification( - context, message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(), ); } @@ -112,7 +111,6 @@ extension PasteFromImage on EditorState { if (errorMessage != null && context.mounted) { showToastNotification( - context, message: errorMessage, ); return false; @@ -131,7 +129,6 @@ extension PasteFromImage on EditorState { Log.error('cannot copy image file', e); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.document_imageBlock_error_invalidImage.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart index 0d71611516..1abf70028f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart @@ -241,7 +241,6 @@ class _LinkHoverTriggerState extends State { .setData(ClipboardServiceData(plainText: href)); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.shareAction_copyLinkSuccess.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index 4394ff57c6..6c09ca6a28 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -154,7 +154,6 @@ class _ErrorBlockComponentWidgetState extends State void _copyBlockContent() { showToastNotification( - context, message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart index 3ab93b4c95..d7ae7c5ccf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart @@ -105,7 +105,6 @@ Future downloadMediaFile( } else { if (userProfile == null) { return showToastNotification( - context, message: LocaleKeys.grid_media_downloadFailedToken.tr(), ); } @@ -128,14 +127,12 @@ Future 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(), ); @@ -159,13 +156,11 @@ Future 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(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart index bbb9dc2abc..7f0105134d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart @@ -378,7 +378,6 @@ class CustomImageBlockComponentState extends State onTap: () async { context.pop(); showToastNotification( - context, message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), ); await getIt().setPlainText(url); @@ -431,7 +430,6 @@ class CustomImageBlockComponentState extends State ); if (mounted) { showToastNotification( - context, message: result.isSuccess ? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr() : LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart index 4a6260d8b8..d11d943066 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart @@ -117,14 +117,12 @@ class _ImageMenuState extends State { 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, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart index b98e05f231..dc95054e81 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart @@ -218,7 +218,6 @@ class _MultiImageMenuState extends State { ClipboardData(text: images[widget.indexNotifier.value].url), ); showToastNotification( - context, message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart index cf7d72cc2a..2ba67a87da 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart @@ -78,7 +78,6 @@ class _LinkPreviewMenuState extends State { if (url != null) { Clipboard.setData(ClipboardData(text: url)); showToastNotification( - context, message: LocaleKeys.document_plugins_urlPreview_copiedToPasteBoard.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart index 117e09e67f..77f8c8d0a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart @@ -100,7 +100,6 @@ class ChildPageTransactionHandler extends MentionTransactionHandler { Log.error(error); if (context.mounted) { showToastNotification( - context, message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage .tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index e4a23b6459..9d6adee7df 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -174,11 +174,10 @@ class ExportTab extends StatelessWidget { ClipboardServiceData(plainText: markdown), ); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); }, - (error) => showToastNotification(context, message: error.msg), + (error) => showToastNotification(message: error.msg), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart index 0ffa22a43b..244ded0bf6 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart @@ -85,11 +85,9 @@ class PublishTab 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, ), @@ -97,11 +95,9 @@ class PublishTab 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, @@ -110,14 +106,12 @@ class PublishTab extends StatelessWidget { } else if (state.updatePathNameResult != null) { state.updatePathNameResult!.fold( (value) => showToastNotification( - context, message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), ), (error) { Log.error('update path name failed: $error'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(), type: ToastificationType.error, description: error.code.publishErrorMessage, @@ -182,7 +176,6 @@ class _PublishedWidgetState extends State<_PublishedWidget> { ); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); }, @@ -292,7 +285,6 @@ class _PublishWidgetState extends State<_PublishWidget> { // check if any database is selected if (_selectedViews.isEmpty) { showToastNotification( - context, message: LocaleKeys.publish_noDatabaseSelected.tr(), ); return; @@ -611,7 +603,6 @@ class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> { // unable to deselect the primary database if (isPrimaryDatabase) { showToastNotification( - context, message: LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart index 59ee55b980..9020441b4e 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart @@ -70,7 +70,6 @@ class ShareButton extends StatelessWidget { case ShareType.html: case ShareType.csv: showToastNotification( - context, message: LocaleKeys.settings_files_exportFileSuccess.tr(), ); break; @@ -81,7 +80,6 @@ class ShareButton extends StatelessWidget { void _handleExportError(BuildContext context, FlowyError error) { showToastNotification( - context, message: '${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}', ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart index 1b925cfec6..190fe9ddd8 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart @@ -117,7 +117,6 @@ class _ShareTabContent extends StatelessWidget { ); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart index da9f679f56..5c5e788ddb 100644 --- a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart +++ b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart @@ -45,7 +45,6 @@ class _MobileSyncErrorPage extends StatelessWidget { onTapUp: () { getIt().setPlainText(error.toString()); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), bottomPadding: 0, ); @@ -101,7 +100,7 @@ class _DesktopSyncErrorPage extends StatelessWidget { onTapUp: () { getIt().setPlainText(error.toString()); showToastNotification( - context, + message: LocaleKeys.message_copy_success.tr(), bottomPadding: 0, ); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 2c22b8a01e..18baef4cb8 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -129,7 +129,7 @@ class AppFlowyCloudDeepLink { final context = AppGlobals.rootNavKey.currentState?.context; if (context != null) { showToastNotification( - context, + message: err.msg, ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart index 2e8e4feeae..51c693843f 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart @@ -17,14 +17,14 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) { case ErrorCode.InvalidEncryptSecret: case ErrorCode.NetworkError: showToastNotification( - context, + message: error.msg, type: ToastificationType.error, ); break; default: showToastNotification( - context, + message: error.msg, type: ToastificationType.error, callbacks: ToastificationCallbacks( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart index 0486d67838..69c9689823 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart @@ -65,7 +65,7 @@ class _SignInWithMagicLinkButtonsState void _sendMagicLink(BuildContext context, String email) { if (!isEmail(email)) { return showToastNotification( - context, + message: LocaleKeys.signIn_invalidEmail.tr(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/util/share_log_files.dart b/frontend/appflowy_flutter/lib/util/share_log_files.dart index d7e7b6ce87..0435c5fe52 100644 --- a/frontend/appflowy_flutter/lib/util/share_log_files.dart +++ b/frontend/appflowy_flutter/lib/util/share_log_files.dart @@ -25,7 +25,7 @@ Future shareLogFiles(BuildContext? context) async { if (archiveLogFiles.isEmpty) { if (context != null && context.mounted) { showToastNotification( - context, + message: LocaleKeys.noLogFiles.tr(), type: ToastificationType.error, ); @@ -42,7 +42,7 @@ Future shareLogFiles(BuildContext? context) async { if (zip == null) { if (context != null && context.mounted) { showToastNotification( - context, + message: LocaleKeys.noLogFiles.tr(), type: ToastificationType.error, ); @@ -72,7 +72,7 @@ Future shareLogFiles(BuildContext? context) async { } catch (e) { if (context != null && context.mounted) { showToastNotification( - context, + message: e.toString(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart index b64ef7d5b8..99b9eaa2c9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; @@ -17,6 +19,7 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:universal_platform/universal_platform.dart'; part 'appearance_cubit.freezed.dart'; @@ -97,7 +100,19 @@ class AppearanceSettingsCubit extends Cubit { Future setTheme(String themeName) async { _appearanceSettings.theme = themeName; unawaited(_saveAppearanceSettings()); - emit(state.copyWith(appTheme: await AppTheme.fromName(themeName))); + try { + final theme = await AppTheme.fromName(themeName); + emit(state.copyWith(appTheme: theme)); + } catch (e) { + Log.error("Error setting theme: $e"); + if (UniversalPlatform.isMacOS) { + showToastNotification( + message: + LocaleKeys.settings_workspacePage_theme_failedToLoadThemes.tr(), + type: ToastificationType.error, + ); + } + } } /// Reset the current user selected theme back to the default diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 038ec9f5a6..20160d32ec 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -169,7 +169,7 @@ class _SidebarWorkspaceState extends State { if (message != null) { showToastNotification( - context, + message: message, type: result.fold( (_) => ToastificationType.success, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index 31139492ad..8cabffc6e3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -193,7 +193,7 @@ Future deleteMyAccount( if (!isChecked) { showToastNotification( - context, + type: ToastificationType.warning, bottomPadding: bottomPadding, message: LocaleKeys @@ -208,7 +208,7 @@ Future deleteMyAccount( if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) { showToastNotification( - context, + type: ToastificationType.warning, bottomPadding: bottomPadding, message: LocaleKeys @@ -226,7 +226,7 @@ Future deleteMyAccount( loading.stop(); showToastNotification( - context, + message: LocaleKeys .newSettings_myAccount_deleteAccount_deleteAccountSuccess .tr(), @@ -245,7 +245,7 @@ Future deleteMyAccount( loading.stop(); showToastNotification( - context, + type: ToastificationType.error, bottomPadding: bottomPadding, message: f.msg, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index 7c096b4b2f..194ad02beb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -157,7 +157,7 @@ class SettingsManageDataView extends StatelessWidget { if (context.mounted) { showToastNotification( - context, + message: LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart index 1a3b305c0b..2f03fc052c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart @@ -53,7 +53,6 @@ class SettingsPageSitesEvent { ); getIt().setData(ClipboardServiceData(plainText: url)); showToastNotification( - context, message: LocaleKeys.message_copy_success.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart index 8f9df5f1b6..b1d9b9cdae 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart @@ -253,7 +253,6 @@ class _FreePlanUpgradeButton extends StatelessWidget { onTap: () { if (isOwner) { showToastNotification( - context, message: LocaleKeys.settings_sites_namespace_redirectToPayment.tr(), type: ToastificationType.info, @@ -264,7 +263,6 @@ class _FreePlanUpgradeButton extends StatelessWidget { ); } else { showToastNotification( - context, message: LocaleKeys .settings_sites_namespace_pleaseAskOwnerToSetHomePage .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart index 6555494144..9617f2c8d6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart @@ -216,7 +216,6 @@ class _DomainSettingsDialogState extends State { result.fold( (s) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(), ); @@ -234,7 +233,6 @@ class _DomainSettingsDialogState extends State { Log.error('Failed to update namespace: $f'); showToastNotification( - context, message: basicErrorMessage, type: ToastificationType.error, description: errorMessage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart index b7f3cecebf..ad37bae866 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart @@ -203,7 +203,6 @@ class _PublishedViewSettingsDialogState result.fold( (s) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), ); Navigator.of(context).pop(); @@ -212,7 +211,6 @@ class _PublishedViewSettingsDialogState Log.error('update path name failed: $f'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(), type: ToastificationType.error, description: f.code.publishErrorMessage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart index 7b00e652ed..f3845b0896 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart @@ -178,7 +178,6 @@ class _SettingsSitesPageView extends StatelessWidget { Log.error('Failed to generate payment link for Pro Plan: ${f.msg}'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_failedToGeneratePaymentLink.tr(), type: ToastificationType.error, @@ -188,14 +187,12 @@ class _SettingsSitesPageView extends StatelessWidget { result != null) { result.fold((_) { showToastNotification( - context, message: LocaleKeys.publish_unpublishSuccessfully.tr(), ); }, (f) { Log.error('Failed to unpublish view: ${f.msg}'); showToastNotification( - context, message: LocaleKeys.publish_unpublishFailed.tr(), type: ToastificationType.error, description: f.msg, @@ -204,14 +201,12 @@ class _SettingsSitesPageView extends StatelessWidget { } else if (type == SettingsSitesActionType.setHomePage && result != null) { result.fold((s) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_setHomepageSuccess.tr(), ); }, (f) { Log.error('Failed to set homepage: ${f.msg}'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_setHomepageFailed.tr(), type: ToastificationType.error, ); @@ -220,14 +215,12 @@ class _SettingsSitesPageView extends StatelessWidget { result != null) { result.fold((s) { showToastNotification( - context, message: LocaleKeys.settings_sites_success_removeHomePageSuccess.tr(), ); }, (f) { Log.error('Failed to remove homepage: ${f.msg}'); showToastNotification( - context, message: LocaleKeys.settings_sites_error_removeHomePageFailed.tr(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 2cf83276b4..08747b95da 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -363,7 +363,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { }) async { if (cloudUrl.isEmpty || webUrl.isEmpty) { showToastNotification( - context, message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(), type: ToastificationType.error, ); @@ -375,7 +374,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { if (mounted) { if (isValid) { showToastNotification( - context, message: LocaleKeys.settings_menu_changeUrl.tr(args: [cloudUrl]), ); @@ -387,7 +385,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { await runAppFlowy(); } else { showToastNotification( - context, message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(), type: ToastificationType.error, ); @@ -522,7 +519,6 @@ class _SupportSettings extends StatelessWidget { await getIt().clearAllCache(); if (context.mounted) { showToastNotification( - context, message: LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index aa541b902c..3bcc840582 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -157,7 +157,6 @@ class _NavigatorTextFieldDialogState extends State { onOkPressed: () { if (newValue.isEmpty) { showToastNotification( - context, message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(), ); return; @@ -363,8 +362,7 @@ class OkCancelButton extends StatelessWidget { } } -void showToastNotification( - BuildContext context, { +void showToastNotification({ String? message, TextSpan? richMessage, String? description, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart index 923f695188..f6a2caa5a2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart @@ -51,7 +51,6 @@ class FlowyVersionSection extends CustomActionCell { } enableDocumentInternalLog = !enableDocumentInternalLog; showToastNotification( - context, message: enableDocumentInternalLog ? 'Enabled Internal Log' : 'Disabled Internal Log', diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 04ae9b30ad..3be0973123 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -74,7 +74,6 @@ class ViewTitleBar extends StatelessWidget { listener: (context, state) { if (state.isLocked) { showToastNotification( - context, message: LocaleKeys.lockPage_pageLockedToast.tr(), ); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 47a2bec391..855a715032 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -628,7 +628,8 @@ "theme": { "title": "Theme", "description": "Select a preset theme, or upload your own custom theme.", - "uploadCustomThemeTooltip": "Upload a custom theme" + "uploadCustomThemeTooltip": "Upload a custom theme", + "failedToLoadThemes": "Failed to load themes, please check your permission settings in System Settings > Privacy and Security > Files and Folders > @:appName" }, "workspaceFont": { "title": "Workspace font", From 89525b7f7b4e5e7b4b21bd99af3f776015242158 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:11:33 +0800 Subject: [PATCH 4/7] chore: bump version (#7716) --- frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 0b23c5df26..41fdffb1af 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -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.8" +APPFLOWY_VERSION = "0.8.9" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0dc9ec673c..9b0987d0b2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -2,13 +2,13 @@ name: appflowy description: Bring projects, wikis, and teams together with AI. AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data. The best open source alternative to Notion. -publish_to: "none" +publish_to: 'none' -version: 0.8.8 +version: 0.8.9 environment: - flutter: ">=3.27.4" - sdk: ">=3.3.0 <4.0.0" + flutter: '>=3.27.4' + sdk: '>=3.3.0 <4.0.0' dependencies: any_date: ^1.0.4 @@ -38,7 +38,7 @@ dependencies: calendar_view: git: url: https://github.com/Xazin/flutter_calendar_view - ref: "6fe0c98" + ref: '6fe0c98' collection: ^1.17.1 connectivity_plus: ^5.0.2 cross_file: ^0.3.4+1 @@ -74,7 +74,7 @@ dependencies: flutter_emoji_mart: git: url: https://github.com/LucasXu0/emoji_mart.git - ref: "355aa56" + ref: '355aa56' flutter_math_fork: ^0.7.3 flutter_slidable: ^3.0.0 @@ -184,13 +184,13 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "361b99c38370abeeb19656f89e8c31cb3666623b" + ref: '361b99c38370abeeb19656f89e8c31cb3666623b' appflowy_editor_plugins: git: url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git - path: "packages/appflowy_editor_plugins" - ref: "4efcff7" + path: 'packages/appflowy_editor_plugins' + ref: '4efcff7' sheet: git: From 9ec04376734cdf225f3cde02b9507b92fa6039c9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 9 Apr 2025 14:15:37 +0800 Subject: [PATCH 5/7] chore: remove enable sync log toggle --- .../presentation/settings/widgets/setting_appflowy_cloud.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index f423156025..5f158f4ae1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -71,7 +71,7 @@ class AppFlowyCloudViewSetting extends StatelessWidget { const VSpace(8), const AppFlowyCloudEnableSync(), const VSpace(6), - const AppFlowyCloudSyncLogEnabled(), + // const AppFlowyCloudSyncLogEnabled(), const VSpace(12), RestartButton( onClick: () { From 2371d4691df487f65b2dda0bd33402448275fbd9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 9 Apr 2025 20:07:06 +0800 Subject: [PATCH 6/7] chore: fix local ai setting --- .../settings/pages/setting_ai_view/local_ai_setting.dart | 6 ++---- .../pages/setting_ai_view/plugin_status_indicator.dart | 4 ++++ frontend/resources/translations/en.json | 4 ++-- frontend/rust-lib/flowy-ai/src/local_ai/resource.rs | 9 +++++++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 751e7a6180..b836f15b03 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -140,10 +140,8 @@ class LocalAISettingPanel extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const LocalAIStatusIndicator(), - if (state.showSettings) ...[ - const VSpace(10), - OllamaSettingPage(), - ], + const VSpace(10), + OllamaSettingPage(), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart index fdf03bb9ca..a280cf0644 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart @@ -288,6 +288,10 @@ class _LackOfResource extends StatelessWidget { text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(), style: textStyle, ), + TextSpan( + text: modelNames.join(', '), + style: textStyle, + ), TextSpan( text: ' ', style: textStyle, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 855a715032..43a59974fd 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -885,7 +885,7 @@ "pleaseFollowThese": "Please follow these", "instructions": "instructions", "installOllamaLai": "to set up Ollama and AppFlowy Local AI.", - "modelsMissing": "Cannot find the required models.", + "modelsMissing": "Cannot find the required models: ", "downloadModel": "to download them." } }, @@ -3209,4 +3209,4 @@ "rewrite": "Rewrite", "insertBelow": "Insert below" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index b384b30f4a..f90c360ec7 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -195,9 +195,14 @@ impl LocalAIResourceController { let tags: TagsResponse = resp.json().await.inspect_err(|e| { log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {e:?}") })?; - // Check each required model is present in the response. + // Check if each of our required models exists in the list of available models + trace!("[LLM Resource] ollama available models: {:?}", tags.models); for required in &required_models { - if !tags.models.iter().any(|m| m.name.contains(required)) { + if !tags + .models + .iter() + .any(|m| m.name == *required || m.name == format!("{}:latest", required)) + { log::trace!( "[LLM Resource] required model '{}' not found in API response", required From a7adeeadb17863630933490e119a5fe8ef08bd88 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 9 Apr 2025 20:47:03 +0800 Subject: [PATCH 7/7] chore: remove local ai related path to lai repo --- frontend/rust-lib/Cargo.lock | 10 +- frontend/rust-lib/Cargo.toml | 6 +- frontend/rust-lib/flowy-ai/Cargo.toml | 5 - .../flowy-ai/src/local_ai/controller.rs | 2 +- .../flowy-ai/src/local_ai/resource.rs | 2 +- .../rust-lib/flowy-ai/src/local_ai/watch.rs | 133 +----------------- 6 files changed, 9 insertions(+), 149 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a72f4596f6..a251594ef9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,12 +345,11 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" dependencies = [ "af-plugin", "anyhow", "bytes", - "futures", "reqwest 0.11.27", "serde", "serde_json", @@ -358,14 +357,12 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", - "zip 2.2.0", - "zip-extensions", ] [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" dependencies = [ "anyhow", "futures-util", @@ -379,7 +376,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3" dependencies = [ "anyhow", "cfg-if", @@ -2504,7 +2501,6 @@ dependencies = [ "tracing-subscriber", "uuid", "validator 0.18.1", - "winreg 0.55.0", "zip 2.2.0", "zip-extensions", ] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 78f67f8f90..4df29d1fe4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" } diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index ee056021c4..a08eae7e92 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -53,11 +53,6 @@ collab-integrate.workspace = true notify = "6.1.1" af-mcp = { version = "0.1.0" } -[target.'cfg(target_os = "windows")'.dependencies] -winreg = "0.55" - -#cmd_lib = { version = "1.9.5" } - [dev-dependencies] dotenv = "0.15.0" uuid.workspace = true diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 6fbe7ce2fc..ac44e9ad55 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -13,9 +13,9 @@ use futures::Sink; use lib_infra::async_trait::async_trait; use std::collections::HashMap; -use crate::local_ai::watch::is_plugin_ready; use crate::stream_message::StreamMessage; use af_local_ai::ollama_plugin::OllamaAIPlugin; +use af_plugin::core::path::is_plugin_ready; use af_plugin::core::plugin::RunningState; use arc_swap::ArcSwapOption; use futures_util::SinkExt; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs index f90c360ec7..2fc6fad2cc 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs @@ -5,13 +5,13 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_infra::async_trait::async_trait; use crate::entities::LackOfAIResourcePB; -use crate::local_ai::watch::{is_plugin_ready, ollama_plugin_path}; #[cfg(target_os = "macos")] use crate::local_ai::watch::{watch_offline_app, WatchContext}; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; use af_local_ai::ollama_plugin::OllamaPluginConfig; +use af_plugin::core::path::{is_plugin_ready, ollama_plugin_path}; use lib_infra::util::{get_operating_system, OperatingSystem}; use reqwest::Client; use serde::Deserialize; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index 1d7be0daf9..2baed3f0a5 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -1,13 +1,10 @@ use crate::local_ai::resource::WatchDiskEvent; +use af_plugin::core::path::{install_path, ollama_plugin_path}; use flowy_error::{FlowyError, FlowyResult}; use std::path::PathBuf; -use std::process::Command; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tracing::{error, trace}; -#[cfg(windows)] -use winreg::{enums::*, RegKey}; - #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] #[allow(dead_code)] pub struct WatchContext { @@ -61,131 +58,3 @@ pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver Option { - None -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub(crate) fn install_path() -> Option { - #[cfg(target_os = "windows")] - return None; - - #[cfg(target_os = "macos")] - return Some(PathBuf::from("/usr/local/bin")); - - #[cfg(target_os = "linux")] - return None; -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub fn is_plugin_ready() -> bool { - ollama_plugin_path().exists() || ollama_plugin_command_available() -} - -#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -pub fn is_plugin_ready() -> bool { - false -} - -#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -pub(crate) fn ollama_plugin_path() -> PathBuf { - PathBuf::new() -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub(crate) fn ollama_plugin_path() -> std::path::PathBuf { - #[cfg(target_os = "windows")] - { - // Use LOCALAPPDATA for a user-specific installation path on Windows. - let local_appdata = - std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Program Files".to_string()); - std::path::PathBuf::from(local_appdata).join("Programs\\appflowy_plugin\\af_ollama_plugin.exe") - } - - #[cfg(target_os = "macos")] - { - let offline_app = "af_ollama_plugin"; - std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) - } - - #[cfg(target_os = "linux")] - { - let offline_app = "af_ollama_plugin"; - std::path::PathBuf::from(format!("/usr/local/bin/{}", offline_app)) - } -} - -pub(crate) fn ollama_plugin_command_available() -> bool { - if cfg!(windows) { - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - let output = Command::new("cmd") - .args(&["/C", "where", "af_ollama_plugin"]) - .creation_flags(CREATE_NO_WINDOW) - .output(); - if let Ok(output) = output { - if !output.stdout.is_empty() { - return true; - } - } - - // 2. Fallback: Check registry PATH for the executable - let path_dirs = get_windows_path_dirs(); - let plugin_exe = "af_ollama_plugin.exe"; // Adjust name if needed - - path_dirs.iter().any(|dir| { - let full_path = std::path::Path::new(dir).join(plugin_exe); - full_path.exists() - }) - } - - #[cfg(not(windows))] - false - } else { - let output = Command::new("command") - .args(["-v", "af_ollama_plugin"]) - .output(); - match output { - Ok(o) => !o.stdout.is_empty(), - _ => false, - } - } -} - -#[cfg(windows)] -fn get_windows_path_dirs() -> Vec { - let mut paths = Vec::new(); - - // Check HKEY_CURRENT_USER\Environment - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - if let Ok(env) = hkcu.open_subkey("Environment") { - if let Ok(path) = env.get_value::("Path") { - paths.extend(path.split(';').map(|s| s.trim().to_string())); - } - } - - // Check HKEY_LOCAL_MACHINE\SYSTEM\...\Environment - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - if let Ok(env) = hklm.open_subkey(r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment") - { - if let Ok(path) = env.get_value::("Path") { - paths.extend(path.split(';').map(|s| s.trim().to_string())); - } - } - paths -} - -#[cfg(test)] -mod tests { - use crate::local_ai::watch::ollama_plugin_command_available; - - #[test] - fn test_command_import() { - let result = ollama_plugin_command_available(); - println!("ollama plugin exist: {:?}", result); - } -}