mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 14:47:13 -04:00
Merge branch 'main' into feat/link_preview
This commit is contained in:
commit
b844aebd00
71 changed files with 883 additions and 316 deletions
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -133,7 +133,6 @@ Future<bool> _afLaunchLocalUri(
|
|||
};
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: message,
|
||||
type: result.type == ResultType.done
|
||||
? ToastificationType.success
|
||||
|
|
|
@ -336,7 +336,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
|||
listener: (context, state) {
|
||||
if (state.isLocked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||
);
|
||||
|
||||
|
@ -366,7 +365,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
|||
listener: (context, state) {
|
||||
if (state.isLocked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -65,7 +65,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
|||
Navigator.pop(context);
|
||||
context.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_duplicateSuccessfully.tr(),
|
||||
);
|
||||
break;
|
||||
|
@ -84,7 +83,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
|||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(widget.view));
|
||||
showToastNotification(
|
||||
context,
|
||||
message: !widget.view.isFavorite
|
||||
? LocaleKeys.button_favoriteSuccessfully.tr()
|
||||
: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
||||
|
@ -146,7 +144,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
|||
Navigator.pop(context);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.sideBar_removeSuccess.tr(),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
|
|||
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})');
|
||||
|
|
|
@ -329,7 +329,7 @@ class _HomePageState extends State<_HomePage> {
|
|||
}
|
||||
|
||||
if (message != null) {
|
||||
showToastNotification(context, message: message, type: toastType);
|
||||
showToastNotification(message: message, type: toastType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -339,7 +339,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
|||
context.read<SpaceBloc>().add(const SpaceEvent.duplicate());
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_success_duplicateSpace.tr(),
|
||||
);
|
||||
|
||||
|
@ -374,7 +373,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
|||
.add(SpaceEvent.rename(space: widget.space, name: name));
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_success_renameSpace.tr(),
|
||||
);
|
||||
},
|
||||
|
@ -424,7 +422,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
|||
);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_success_updateSpace.tr(),
|
||||
);
|
||||
|
||||
|
@ -457,7 +454,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
|||
context.read<SpaceBloc>().add(SpaceEvent.delete(widget.space));
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_success_deleteSpace.tr(),
|
||||
);
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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)',
|
||||
);
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -74,7 +74,6 @@ class _NotificationTabState extends State<NotificationTab>
|
|||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_notifications_refreshSuccess.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -81,7 +81,6 @@ class SupportSettingGroup extends StatelessWidget {
|
|||
);
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -184,7 +184,6 @@ class CopyButton extends StatelessWidget {
|
|||
);
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -376,7 +376,6 @@ class ChatAIMessagePopup extends StatelessWidget {
|
|||
}
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -442,7 +442,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||
final context = AppGlobals.rootNavKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: 'document integrity check failed',
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
|
|
@ -150,7 +150,6 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
|
|||
});
|
||||
} 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(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<AiWriterState> {
|
||||
AiWriterCubit({
|
||||
required this.documentId,
|
||||
|
@ -95,6 +101,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
final command = node.aiWriterCommand;
|
||||
final (run, prompt) = await _addSelectionTextToRecords(command);
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'command: $command, run: $run, prompt: $prompt',
|
||||
);
|
||||
|
||||
if (!run) {
|
||||
await exit();
|
||||
return;
|
||||
|
@ -211,20 +221,26 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Accept
|
||||
//
|
||||
// If the user clicks accept, we need to replace the selection with the AI's response
|
||||
if (action case SuggestionAction.accept) {
|
||||
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<AiWriterState> {
|
|||
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<AiWriterState> {
|
|||
records.add(
|
||||
AiWriterRecord.user(content: selectionText, format: null),
|
||||
);
|
||||
|
||||
return (true, '');
|
||||
} else {
|
||||
return (true, selectionText);
|
||||
|
@ -540,6 +564,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
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<AiWriterState> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'received assist message: $text',
|
||||
);
|
||||
},
|
||||
onEnd: () async {
|
||||
if (state case final GeneratingAiWriterState generatingState) {
|
||||
|
@ -567,6 +599,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
records.add(
|
||||
AiWriterRecord.ai(content: _textRobot.markdownText),
|
||||
);
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'returned response: ${_textRobot.markdownText}',
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) async {
|
||||
|
@ -658,6 +694,12 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _aiWriterCubitLog(String message) {
|
||||
if (_aiWriterCubitDebugLog) {
|
||||
Log.debug('[AiWriterCubit] $message');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mixin RegisteredAiWriter {
|
||||
|
|
|
@ -57,11 +57,30 @@ extension AiWriterNodeExtension on EditorState {
|
|||
slicedNodes.add(copiedNode);
|
||||
}
|
||||
|
||||
for (final (i, node) in slicedNodes.indexed) {
|
||||
final childNodesShouldBeDeleted = <Node>[];
|
||||
for (final child in node.children) {
|
||||
if (!child.path.inSelection(selection)) {
|
||||
childNodesShouldBeDeleted.add(child);
|
||||
}
|
||||
}
|
||||
for (final child in childNodesShouldBeDeleted) {
|
||||
slicedNodes[i] = node.copyWith(
|
||||
children: node.children.where((e) => e.id != child.id).toList(),
|
||||
type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// use \n\n as line break to improve the ai response
|
||||
// using \n will cause the ai response treat the text as a single line
|
||||
final markdown = await customDocumentToMarkdown(
|
||||
Document.blank()..insert([0], slicedNodes),
|
||||
lineBreak: '\n\n',
|
||||
);
|
||||
|
||||
return markdown;
|
||||
// trim the last \n if it exists
|
||||
return markdown.trimRight();
|
||||
}
|
||||
|
||||
List<String> getPlainTextInSelection(Selection? selection) {
|
||||
|
|
|
@ -110,10 +110,13 @@ class MarkdownTextRobot {
|
|||
}
|
||||
|
||||
/// Persist the text into the document
|
||||
Future<void> persist({String? markdownText}) async {
|
||||
Future<void> persist({
|
||||
String? markdownText,
|
||||
}) async {
|
||||
if (markdownText != null) {
|
||||
_markdownText = markdownText;
|
||||
}
|
||||
|
||||
await _lock.synchronized(() async {
|
||||
await _refresh(inMemoryUpdate: false);
|
||||
});
|
||||
|
@ -124,6 +127,34 @@ class MarkdownTextRobot {
|
|||
}
|
||||
}
|
||||
|
||||
/// Replace the selected content with the AI's response
|
||||
Future<void> replace({
|
||||
required Selection selection,
|
||||
required String markdownText,
|
||||
}) async {
|
||||
if (selection.isSingle) {
|
||||
await _replaceInSameLine(
|
||||
selection: selection,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
} else {
|
||||
await _replaceInMultiLines(
|
||||
selection: selection,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the temporary inserted AI nodes
|
||||
Future<void> deleteAINodes() async {
|
||||
final nodes = getInsertedNodes();
|
||||
final transaction = editorState.transaction..deleteNodes(nodes);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(recordUndo: false),
|
||||
);
|
||||
}
|
||||
|
||||
/// Discard the inserted content
|
||||
Future<void> discard() 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<void> _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<void> _replaceInMultiLines({
|
||||
required Selection selection,
|
||||
required String markdownText,
|
||||
}) async {
|
||||
selection = selection.normalized;
|
||||
|
||||
// If the selection is a single node, do nothing.
|
||||
if (selection.isSingle) {
|
||||
assert(false, 'Expected multi-line selection');
|
||||
Log.error('Expected multi-line selection');
|
||||
return;
|
||||
}
|
||||
|
||||
final markdownNodes = customMarkdownToDocument(
|
||||
markdownText,
|
||||
tableWidth: 250.0,
|
||||
).root.children;
|
||||
|
||||
// Get the selected nodes.
|
||||
final 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 {
|
||||
|
|
|
@ -47,7 +47,6 @@ class _CopyButton extends StatelessWidget {
|
|||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -154,7 +154,6 @@ class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
|||
|
||||
void _copyBlockContent() {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),
|
||||
);
|
||||
|
||||
|
|
|
@ -105,7 +105,6 @@ Future<void> downloadMediaFile(
|
|||
} else {
|
||||
if (userProfile == null) {
|
||||
return showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_media_downloadFailedToken.tr(),
|
||||
);
|
||||
}
|
||||
|
@ -128,14 +127,12 @@ Future<void> downloadMediaFile(
|
|||
|
||||
if (result != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.grid_media_downloadSuccess.tr(),
|
||||
);
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
||||
);
|
||||
|
@ -159,13 +156,11 @@ Future<void> downloadMediaFile(
|
|||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_media_downloadSuccess.tr(),
|
||||
);
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
||||
);
|
||||
|
|
|
@ -378,7 +378,6 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
|
|||
onTap: () async {
|
||||
context.pop();
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
||||
);
|
||||
await getIt<ClipboardService>().setPlainText(url);
|
||||
|
@ -431,7 +430,6 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
|
|||
);
|
||||
if (mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: result.isSuccess
|
||||
? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr()
|
||||
: LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(),
|
||||
|
|
|
@ -117,14 +117,12 @@ class _ImageMenuState extends State<ImageMenu> {
|
|||
|
||||
if (mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_fail.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
|
|
@ -218,7 +218,6 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
|
|||
ClipboardData(text: images[widget.indexNotifier.value].url),
|
||||
);
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CustomLinkPreviewMenu extends StatefulWidget {
|
||||
|
@ -122,14 +121,7 @@ class _CustomLinkPreviewMenuState extends State<CustomLinkPreviewMenu> {
|
|||
break;
|
||||
case LinkPreviewMenuCommand.copyLink:
|
||||
if (url != null) {
|
||||
await Clipboard.setData(ClipboardData(text: url));
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||
);
|
||||
}
|
||||
await context.copyLink(url);
|
||||
}
|
||||
break;
|
||||
case LinkPreviewMenuCommand.replace:
|
||||
|
|
|
@ -100,7 +100,6 @@ class ChildPageTransactionHandler extends MentionTransactionHandler {
|
|||
Log.error(error);
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage
|
||||
.tr(),
|
||||
);
|
||||
|
|
|
@ -38,10 +38,6 @@ Future<bool> 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<bool> emojiCommandHandler(
|
|||
if (previousCharacter != _emojiCharacter) return false;
|
||||
if (!context.mounted) return false;
|
||||
|
||||
if (!selection.isCollapsed) return false;
|
||||
|
||||
await editorState.insertTextAtPosition(
|
||||
character,
|
||||
position: selection.start,
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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}',
|
||||
);
|
||||
|
|
|
@ -117,7 +117,6 @@ class _ShareTabContent extends StatelessWidget {
|
|||
);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ class _MobileSyncErrorPage extends StatelessWidget {
|
|||
onTapUp: () {
|
||||
getIt<ClipboardService>().setPlainText(error.toString());
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
bottomPadding: 0,
|
||||
);
|
||||
|
@ -101,7 +100,7 @@ class _DesktopSyncErrorPage extends StatelessWidget {
|
|||
onTapUp: () {
|
||||
getIt<ClipboardService>().setPlainText(error.toString());
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
bottomPadding: 0,
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ Future<String> customDocumentToMarkdown(
|
|||
Document document, {
|
||||
String path = '',
|
||||
AsyncValueSetter<Archive>? onArchive,
|
||||
String lineBreak = '',
|
||||
}) async {
|
||||
final List<Future<ArchiveFile>> fileFutures = [];
|
||||
|
||||
|
@ -41,6 +42,7 @@ Future<String> customDocumentToMarkdown(
|
|||
try {
|
||||
markdown = documentToMarkdown(
|
||||
document,
|
||||
lineBreak: lineBreak,
|
||||
customParsers: [
|
||||
const MathEquationNodeParser(),
|
||||
const CalloutNodeParser(),
|
||||
|
|
|
@ -129,7 +129,7 @@ class AppFlowyCloudDeepLink {
|
|||
final context = AppGlobals.rootNavKey.currentState?.context;
|
||||
if (context != null) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: err.msg,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -25,7 +25,7 @@ Future<void> 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<void> 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<void> shareLogFiles(BuildContext? context) async {
|
|||
} catch (e) {
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: e.toString(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
|
|
@ -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<AppearanceSettingsState> {
|
|||
Future<void> 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
|
||||
|
|
|
@ -169,7 +169,7 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> {
|
|||
|
||||
if (message != null) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: message,
|
||||
type: result.fold(
|
||||
(_) => ToastificationType.success,
|
||||
|
|
|
@ -193,7 +193,7 @@ Future<void> deleteMyAccount(
|
|||
|
||||
if (!isChecked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
type: ToastificationType.warning,
|
||||
bottomPadding: bottomPadding,
|
||||
message: LocaleKeys
|
||||
|
@ -208,7 +208,7 @@ Future<void> deleteMyAccount(
|
|||
|
||||
if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
type: ToastificationType.warning,
|
||||
bottomPadding: bottomPadding,
|
||||
message: LocaleKeys
|
||||
|
@ -226,7 +226,7 @@ Future<void> deleteMyAccount(
|
|||
|
||||
loading.stop();
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: LocaleKeys
|
||||
.newSettings_myAccount_deleteAccount_deleteAccountSuccess
|
||||
.tr(),
|
||||
|
@ -245,7 +245,7 @@ Future<void> deleteMyAccount(
|
|||
|
||||
loading.stop();
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
type: ToastificationType.error,
|
||||
bottomPadding: bottomPadding,
|
||||
message: f.msg,
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -157,7 +157,7 @@ class SettingsManageDataView extends StatelessWidget {
|
|||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: LocaleKeys
|
||||
.settings_manageDataPage_cache_dialog_successHint
|
||||
.tr(),
|
||||
|
|
|
@ -53,7 +53,6 @@ class SettingsPageSitesEvent {
|
|||
);
|
||||
getIt<ClipboardService>().setData(ClipboardServiceData(plainText: url));
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -216,7 +216,6 @@ class _DomainSettingsDialogState extends State<DomainSettingsDialog> {
|
|||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(),
|
||||
);
|
||||
|
||||
|
@ -234,7 +233,6 @@ class _DomainSettingsDialogState extends State<DomainSettingsDialog> {
|
|||
Log.error('Failed to update namespace: $f');
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: basicErrorMessage,
|
||||
type: ToastificationType.error,
|
||||
description: errorMessage,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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<FlowyCacheManager>().clearAllCache();
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys
|
||||
.settings_manageDataPage_cache_dialog_successHint
|
||||
.tr(),
|
||||
|
|
|
@ -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: () {
|
||||
|
|
|
@ -157,7 +157,6 @@ class _NavigatorTextFieldDialogState extends State<NavigatorTextFieldDialog> {
|
|||
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,
|
||||
|
|
|
@ -51,7 +51,6 @@ class FlowyVersionSection extends CustomActionCell {
|
|||
}
|
||||
enableDocumentInternalLog = !enableDocumentInternalLog;
|
||||
showToastNotification(
|
||||
context,
|
||||
message: enableDocumentInternalLog
|
||||
? 'Enabled Internal Log'
|
||||
: 'Disabled Internal Log',
|
||||
|
|
|
@ -74,7 +74,6 @@ class ViewTitleBar extends StatelessWidget {
|
|||
listener: (context, state) {
|
||||
if (state.isLocked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
@ -41,7 +41,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
|
||||
|
@ -77,7 +77,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
|
||||
|
||||
|
@ -187,13 +187,13 @@ dependency_overrides:
|
|||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "552f95f"
|
||||
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:
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
@ -884,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."
|
||||
}
|
||||
},
|
||||
|
|
10
frontend/rust-lib/Cargo.lock
generated
10
frontend/rust-lib/Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
@ -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
|
||||
|
|
|
@ -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<Watch
|
|||
rx,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
pub(crate) fn install_path() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
pub(crate) fn install_path() -> Option<PathBuf> {
|
||||
#[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<String> {
|
||||
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::<String, _>("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::<String, _>("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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue