mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 22:57:12 -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_FS_NAME = "dart_ffi"
|
||||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||||
LIB_NAME = "dart_ffi"
|
LIB_NAME = "dart_ffi"
|
||||||
APPFLOWY_VERSION = "0.8.8"
|
APPFLOWY_VERSION = "0.8.9"
|
||||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||||
PRODUCT_NAME = "AppFlowy"
|
PRODUCT_NAME = "AppFlowy"
|
||||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
@ -47,5 +48,41 @@ void main() {
|
||||||
|
|
||||||
expect(editorState.selection!.start.offset, 0);
|
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) {
|
if (context != null && context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: message,
|
message: message,
|
||||||
type: result.type == ResultType.done
|
type: result.type == ResultType.done
|
||||||
? ToastificationType.success
|
? ToastificationType.success
|
||||||
|
|
|
@ -336,7 +336,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.isLocked) {
|
if (state.isLocked) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -366,7 +365,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.isLocked) {
|
if (state.isLocked) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||||
context.pop();
|
context.pop();
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.button_duplicateSuccessfully.tr(),
|
message: LocaleKeys.button_duplicateSuccessfully.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -170,7 +169,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||||
_toggleFavorite(context);
|
_toggleFavorite(context);
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.button_favoriteSuccessfully.tr(),
|
message: LocaleKeys.button_favoriteSuccessfully.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -179,7 +177,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||||
_toggleFavorite(context);
|
_toggleFavorite(context);
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -202,7 +199,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -234,12 +230,10 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(),
|
message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
@ -323,11 +317,9 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||||
if (state.publishResult != null) {
|
if (state.publishResult != null) {
|
||||||
state.publishResult!.fold(
|
state.publishResult!.fold(
|
||||||
(value) => showToastNotification(
|
(value) => showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_publishSuccessfully.tr(),
|
message: LocaleKeys.publish_publishSuccessfully.tr(),
|
||||||
),
|
),
|
||||||
(error) => showToastNotification(
|
(error) => showToastNotification(
|
||||||
context,
|
|
||||||
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
|
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
),
|
),
|
||||||
|
@ -335,11 +327,9 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||||
} else if (state.unpublishResult != null) {
|
} else if (state.unpublishResult != null) {
|
||||||
state.unpublishResult!.fold(
|
state.unpublishResult!.fold(
|
||||||
(value) => showToastNotification(
|
(value) => showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||||
),
|
),
|
||||||
(error) => showToastNotification(
|
(error) => showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_unpublishFailed.tr(),
|
message: LocaleKeys.publish_unpublishFailed.tr(),
|
||||||
description: error.msg,
|
description: error.msg,
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
|
@ -349,7 +339,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||||
state.updatePathNameResult!.onSuccess(
|
state.updatePathNameResult!.onSuccess(
|
||||||
(value) {
|
(value) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message:
|
message:
|
||||||
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -65,7 +65,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
context.read<ViewBloc>().add(const ViewEvent.duplicate());
|
context.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.button_duplicateSuccessfully.tr(),
|
message: LocaleKeys.button_duplicateSuccessfully.tr(),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -84,7 +83,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
||||||
.read<FavoriteBloc>()
|
.read<FavoriteBloc>()
|
||||||
.add(FavoriteEvent.toggle(widget.view));
|
.add(FavoriteEvent.toggle(widget.view));
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: !widget.view.isFavorite
|
message: !widget.view.isFavorite
|
||||||
? LocaleKeys.button_favoriteSuccessfully.tr()
|
? LocaleKeys.button_favoriteSuccessfully.tr()
|
||||||
: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
||||||
|
@ -146,7 +144,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.sideBar_removeSuccess.tr(),
|
message: LocaleKeys.sideBar_removeSuccess.tr(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,7 +45,6 @@ enum MobilePaneActionType {
|
||||||
size: 24.0,
|
size: 24.0,
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -61,7 +60,6 @@ enum MobilePaneActionType {
|
||||||
size: 24.0,
|
size: 24.0,
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.button_favoriteSuccessfully.tr(),
|
message: LocaleKeys.button_favoriteSuccessfully.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
|
||||||
Log.info('Open row page(${widget.documentId})');
|
Log.info('Open row page(${widget.documentId})');
|
||||||
|
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
showToastNotification(context, message: 'Failed to open row page');
|
showToastNotification(message: 'Failed to open row page');
|
||||||
// reload the view again
|
// reload the view again
|
||||||
unawaited(_preloadView(context));
|
unawaited(_preloadView(context));
|
||||||
Log.error('Failed to open row page(${widget.documentId})');
|
Log.error('Failed to open row page(${widget.documentId})');
|
||||||
|
|
|
@ -329,7 +329,7 @@ class _HomePageState extends State<_HomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message != null) {
|
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());
|
context.read<SpaceBloc>().add(const SpaceEvent.duplicate());
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.space_success_duplicateSpace.tr(),
|
message: LocaleKeys.space_success_duplicateSpace.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -374,7 +373,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
||||||
.add(SpaceEvent.rename(space: widget.space, name: name));
|
.add(SpaceEvent.rename(space: widget.space, name: name));
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.space_success_renameSpace.tr(),
|
message: LocaleKeys.space_success_renameSpace.tr(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -424,7 +422,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
||||||
);
|
);
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.space_success_updateSpace.tr(),
|
message: LocaleKeys.space_success_updateSpace.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -457,7 +454,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
||||||
context.read<SpaceBloc>().add(SpaceEvent.delete(widget.space));
|
context.read<SpaceBloc>().add(SpaceEvent.delete(widget.space));
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.space_success_deleteSpace.tr(),
|
message: LocaleKeys.space_success_deleteSpace.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -332,7 +332,6 @@ class _NotificationNavigationBar extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.settings_notifications_markAsReadNotifications_allSuccess
|
.settings_notifications_markAsReadNotifications_allSuccess
|
||||||
.tr(),
|
.tr(),
|
||||||
|
@ -350,7 +349,6 @@ class _NotificationNavigationBar extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
|
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
|
||||||
.tr(),
|
.tr(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -108,7 +108,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
||||||
|
|
||||||
void _onMarkAllAsRead(BuildContext context) {
|
void _onMarkAllAsRead(BuildContext context) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.settings_notifications_markAsReadNotifications_allSuccess
|
.settings_notifications_markAsReadNotifications_allSuccess
|
||||||
.tr(),
|
.tr(),
|
||||||
|
@ -119,7 +118,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
||||||
|
|
||||||
void _onArchiveAll(BuildContext context) {
|
void _onArchiveAll(BuildContext context) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
|
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
|
||||||
.tr(),
|
.tr(),
|
||||||
);
|
);
|
||||||
|
@ -133,7 +131,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: 'Unarchive all success (Debug Mode)',
|
message: 'Unarchive all success (Debug Mode)',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ enum NotificationPaneActionType {
|
||||||
size: 24.0,
|
size: 24.0,
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.settings_notifications_markAsReadNotifications_success
|
.settings_notifications_markAsReadNotifications_success
|
||||||
.tr(),
|
.tr(),
|
||||||
|
@ -55,7 +54,6 @@ enum NotificationPaneActionType {
|
||||||
size: 24.0,
|
size: 24.0,
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: 'Unarchive notification success',
|
message: 'Unarchive notification success',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -168,7 +166,6 @@ class _NotificationMoreActions extends StatelessWidget {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_notifications_markAsReadNotifications_success
|
message: LocaleKeys.settings_notifications_markAsReadNotifications_success
|
||||||
.tr(),
|
.tr(),
|
||||||
);
|
);
|
||||||
|
@ -191,7 +188,6 @@ class _NotificationMoreActions extends StatelessWidget {
|
||||||
|
|
||||||
void _onArchive(BuildContext context) {
|
void _onArchive(BuildContext context) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_notifications_archiveNotifications_success
|
message: LocaleKeys.settings_notifications_archiveNotifications_success
|
||||||
.tr()
|
.tr()
|
||||||
.tr(),
|
.tr(),
|
||||||
|
|
|
@ -74,7 +74,6 @@ class _NotificationTabState extends State<NotificationTab>
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_notifications_refreshSuccess.tr(),
|
message: LocaleKeys.settings_notifications_refreshSuccess.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,6 @@ class SupportSettingGroup extends StatelessWidget {
|
||||||
);
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
|
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,7 +201,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||||
result.fold(
|
result.fold(
|
||||||
(s) {
|
(s) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message:
|
message:
|
||||||
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
|
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
|
||||||
bottomPadding: keyboardHeight,
|
bottomPadding: keyboardHeight,
|
||||||
|
@ -218,7 +217,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||||
});
|
});
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
bottomPadding: keyboardHeight,
|
bottomPadding: keyboardHeight,
|
||||||
message: message,
|
message: message,
|
||||||
|
@ -229,7 +227,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||||
result.fold(
|
result.fold(
|
||||||
(s) {
|
(s) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message:
|
message:
|
||||||
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
|
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
|
||||||
bottomPadding: keyboardHeight,
|
bottomPadding: keyboardHeight,
|
||||||
|
@ -247,7 +244,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||||
});
|
});
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
message: message,
|
message: message,
|
||||||
bottomPadding: keyboardHeight,
|
bottomPadding: keyboardHeight,
|
||||||
|
@ -258,7 +254,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||||
result.fold(
|
result.fold(
|
||||||
(s) {
|
(s) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.settings_appearance_members_removeFromWorkspaceSuccess
|
.settings_appearance_members_removeFromWorkspaceSuccess
|
||||||
.tr(),
|
.tr(),
|
||||||
|
@ -267,7 +262,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||||
},
|
},
|
||||||
(f) {
|
(f) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.settings_appearance_members_removeFromWorkspaceFailed
|
.settings_appearance_members_removeFromWorkspaceFailed
|
||||||
|
@ -283,7 +277,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||||
final email = emailController.text;
|
final email = emailController.text;
|
||||||
if (!isEmail(email)) {
|
if (!isEmail(email)) {
|
||||||
return showToastNotification(
|
return showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
|
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -184,7 +184,6 @@ class CopyButton extends StatelessWidget {
|
||||||
);
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -376,7 +376,6 @@ class ChatAIMessagePopup extends StatelessWidget {
|
||||||
}
|
}
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import 'package:universal_platform/universal_platform.dart';
|
||||||
void openPageFromMessage(BuildContext context, ViewPB? view) {
|
void openPageFromMessage(BuildContext context, ViewPB? view) {
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),
|
message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
@ -36,7 +35,6 @@ void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
|
|
|
@ -442,7 +442,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||||
final context = AppGlobals.rootNavKey.currentContext;
|
final context = AppGlobals.rootNavKey.currentContext;
|
||||||
if (context != null && context.mounted) {
|
if (context != null && context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: 'document integrity check failed',
|
message: 'document integrity check failed',
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
|
|
@ -150,7 +150,6 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -196,7 +195,6 @@ class ImproveWritingButton extends StatelessWidget {
|
||||||
_insertAiNode(editorState, AiWriterCommand.improveWriting);
|
_insertAiNode(editorState, AiWriterCommand.improveWriting);
|
||||||
} else {
|
} else {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||||
import 'package:appflowy/ai/ai.dart';
|
import 'package:appflowy/ai/ai.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.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_backend/protobuf/flowy-ai/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.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_entities.dart';
|
||||||
import 'ai_writer_node_extension.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> {
|
class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
AiWriterCubit({
|
AiWriterCubit({
|
||||||
required this.documentId,
|
required this.documentId,
|
||||||
|
@ -95,6 +101,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
final command = node.aiWriterCommand;
|
final command = node.aiWriterCommand;
|
||||||
final (run, prompt) = await _addSelectionTextToRecords(command);
|
final (run, prompt) = await _addSelectionTextToRecords(command);
|
||||||
|
|
||||||
|
_aiWriterCubitLog(
|
||||||
|
'command: $command, run: $run, prompt: $prompt',
|
||||||
|
);
|
||||||
|
|
||||||
if (!run) {
|
if (!run) {
|
||||||
await exit();
|
await exit();
|
||||||
return;
|
return;
|
||||||
|
@ -211,20 +221,26 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept
|
||||||
|
//
|
||||||
|
// If the user clicks accept, we need to replace the selection with the AI's response
|
||||||
if (action case SuggestionAction.accept) {
|
if (action case SuggestionAction.accept) {
|
||||||
await _textRobot.persist();
|
// trim the markdown text to avoid extra new lines
|
||||||
await formatSelection(
|
final trimmedMarkdownText = _textRobot.markdownText.trim();
|
||||||
editorState,
|
|
||||||
selection,
|
_aiWriterCubitLog(
|
||||||
ApplySuggestionFormatType.clear,
|
'trigger accept action, markdown text: $trimmedMarkdownText',
|
||||||
);
|
);
|
||||||
final nodes = editorState.getNodesInSelection(selection);
|
|
||||||
final transaction = editorState.transaction..deleteNodes(nodes);
|
await _textRobot.deleteAINodes();
|
||||||
await editorState.apply(
|
|
||||||
transaction,
|
await _textRobot.replace(
|
||||||
withUpdateSelection: false,
|
selection: selection,
|
||||||
|
markdownText: trimmedMarkdownText,
|
||||||
);
|
);
|
||||||
|
|
||||||
await exit(withDiscard: false, withUnformat: false);
|
await exit(withDiscard: false, withUnformat: false);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,17 +292,24 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
AiWriterCommand command,
|
AiWriterCommand command,
|
||||||
) async {
|
) async {
|
||||||
final node = aiWriterNode;
|
final node = aiWriterNode;
|
||||||
|
|
||||||
|
// check the node is registered
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
return (false, '');
|
return (false, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check the selection is valid
|
||||||
final selection = node.aiWriterSelection?.normalized;
|
final selection = node.aiWriterSelection?.normalized;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
return (false, '');
|
return (false, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the command is continue writing, we don't need to get the selection text
|
||||||
if (command == AiWriterCommand.continueWriting) {
|
if (command == AiWriterCommand.continueWriting) {
|
||||||
return (true, '');
|
return (true, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the selection is collapsed, we don't need to get the selection text
|
||||||
if (selection.isCollapsed) {
|
if (selection.isCollapsed) {
|
||||||
return (true, '');
|
return (true, '');
|
||||||
}
|
}
|
||||||
|
@ -297,6 +320,7 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
records.add(
|
records.add(
|
||||||
AiWriterRecord.user(content: selectionText, format: null),
|
AiWriterRecord.user(content: selectionText, format: null),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (true, '');
|
return (true, '');
|
||||||
} else {
|
} else {
|
||||||
return (true, selectionText);
|
return (true, selectionText);
|
||||||
|
@ -540,6 +564,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
attributes: ApplySuggestionFormatType.replace.attributes,
|
attributes: ApplySuggestionFormatType.replace.attributes,
|
||||||
);
|
);
|
||||||
onAppendToDocument?.call();
|
onAppendToDocument?.call();
|
||||||
|
|
||||||
|
_aiWriterCubitLog(
|
||||||
|
'received message: $text',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
processAssistMessage: (text) async {
|
processAssistMessage: (text) async {
|
||||||
if (state case final GeneratingAiWriterState generatingState) {
|
if (state case final GeneratingAiWriterState generatingState) {
|
||||||
|
@ -551,6 +579,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_aiWriterCubitLog(
|
||||||
|
'received assist message: $text',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onEnd: () async {
|
onEnd: () async {
|
||||||
if (state case final GeneratingAiWriterState generatingState) {
|
if (state case final GeneratingAiWriterState generatingState) {
|
||||||
|
@ -567,6 +599,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
records.add(
|
records.add(
|
||||||
AiWriterRecord.ai(content: _textRobot.markdownText),
|
AiWriterRecord.ai(content: _textRobot.markdownText),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_aiWriterCubitLog(
|
||||||
|
'returned response: ${_textRobot.markdownText}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) async {
|
onError: (error) async {
|
||||||
|
@ -658,6 +694,12 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _aiWriterCubitLog(String message) {
|
||||||
|
if (_aiWriterCubitDebugLog) {
|
||||||
|
Log.debug('[AiWriterCubit] $message');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin RegisteredAiWriter {
|
mixin RegisteredAiWriter {
|
||||||
|
|
|
@ -57,11 +57,30 @@ extension AiWriterNodeExtension on EditorState {
|
||||||
slicedNodes.add(copiedNode);
|
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(
|
final markdown = await customDocumentToMarkdown(
|
||||||
Document.blank()..insert([0], slicedNodes),
|
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) {
|
List<String> getPlainTextInSelection(Selection? selection) {
|
||||||
|
|
|
@ -110,10 +110,13 @@ class MarkdownTextRobot {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persist the text into the document
|
/// Persist the text into the document
|
||||||
Future<void> persist({String? markdownText}) async {
|
Future<void> persist({
|
||||||
|
String? markdownText,
|
||||||
|
}) async {
|
||||||
if (markdownText != null) {
|
if (markdownText != null) {
|
||||||
_markdownText = markdownText;
|
_markdownText = markdownText;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _lock.synchronized(() async {
|
await _lock.synchronized(() async {
|
||||||
await _refresh(inMemoryUpdate: false);
|
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
|
/// Discard the inserted content
|
||||||
Future<void> discard() async {
|
Future<void> discard() async {
|
||||||
final start = _insertPosition;
|
final start = _insertPosition;
|
||||||
|
@ -282,6 +313,161 @@ class MarkdownTextRobot {
|
||||||
children: children,
|
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 {
|
class AINodeExternalValues extends NodeExternalValues {
|
||||||
|
|
|
@ -47,7 +47,6 @@ class _CopyButton extends StatelessWidget {
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
|
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,6 @@ extension PasteFromImage on EditorState {
|
||||||
Log.info('unsupported format: $format');
|
Log.info('unsupported format: $format');
|
||||||
if (UniversalPlatform.isMobile) {
|
if (UniversalPlatform.isMobile) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(),
|
message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -112,7 +111,6 @@ extension PasteFromImage on EditorState {
|
||||||
|
|
||||||
if (errorMessage != null && context.mounted) {
|
if (errorMessage != null && context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
|
@ -131,7 +129,6 @@ extension PasteFromImage on EditorState {
|
||||||
Log.error('cannot copy image file', e);
|
Log.error('cannot copy image file', e);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_imageBlock_error_invalidImage.tr(),
|
message: LocaleKeys.document_imageBlock_error_invalidImage.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,7 +154,6 @@ class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
||||||
|
|
||||||
void _copyBlockContent() {
|
void _copyBlockContent() {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),
|
message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,6 @@ Future<void> downloadMediaFile(
|
||||||
} else {
|
} else {
|
||||||
if (userProfile == null) {
|
if (userProfile == null) {
|
||||||
return showToastNotification(
|
return showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.grid_media_downloadFailedToken.tr(),
|
message: LocaleKeys.grid_media_downloadFailedToken.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -128,14 +127,12 @@ Future<void> downloadMediaFile(
|
||||||
|
|
||||||
if (result != null && context.mounted) {
|
if (result != null && context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
message: LocaleKeys.grid_media_downloadSuccess.tr(),
|
message: LocaleKeys.grid_media_downloadSuccess.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (context.mounted) {
|
} else if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
||||||
);
|
);
|
||||||
|
@ -159,13 +156,11 @@ Future<void> downloadMediaFile(
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.grid_media_downloadSuccess.tr(),
|
message: LocaleKeys.grid_media_downloadSuccess.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (context.mounted) {
|
} else if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -378,7 +378,6 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
context.pop();
|
context.pop();
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
||||||
);
|
);
|
||||||
await getIt<ClipboardService>().setPlainText(url);
|
await getIt<ClipboardService>().setPlainText(url);
|
||||||
|
@ -431,7 +430,6 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
|
||||||
);
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: result.isSuccess
|
message: result.isSuccess
|
||||||
? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr()
|
? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr()
|
||||||
: LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(),
|
: LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(),
|
||||||
|
|
|
@ -117,14 +117,12 @@ class _ImageMenuState extends State<ImageMenu> {
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_fail.tr(),
|
message: LocaleKeys.message_copy_fail.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
|
|
@ -218,7 +218,6 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
|
||||||
ClipboardData(text: images[widget.indexNotifier.value].url),
|
ClipboardData(text: images[widget.indexNotifier.value].url),
|
||||||
);
|
);
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/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_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/link_preview/shared.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.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/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class CustomLinkPreviewMenu extends StatefulWidget {
|
class CustomLinkPreviewMenu extends StatefulWidget {
|
||||||
|
@ -122,14 +121,7 @@ class _CustomLinkPreviewMenuState extends State<CustomLinkPreviewMenu> {
|
||||||
break;
|
break;
|
||||||
case LinkPreviewMenuCommand.copyLink:
|
case LinkPreviewMenuCommand.copyLink:
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
await Clipboard.setData(ClipboardData(text: url));
|
await context.copyLink(url);
|
||||||
if (context.mounted) {
|
|
||||||
showToastNotification(
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
context,
|
|
||||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case LinkPreviewMenuCommand.replace:
|
case LinkPreviewMenuCommand.replace:
|
||||||
|
|
|
@ -100,7 +100,6 @@ class ChildPageTransactionHandler extends MentionTransactionHandler {
|
||||||
Log.error(error);
|
Log.error(error);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage
|
message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage
|
||||||
.tr(),
|
.tr(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -38,10 +38,6 @@ Future<bool> emojiCommandHandler(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selection.isCollapsed) {
|
|
||||||
await editorState.deleteSelection(selection);
|
|
||||||
}
|
|
||||||
|
|
||||||
final node = editorState.getNodeAtPath(selection.end.path);
|
final node = editorState.getNodeAtPath(selection.end.path);
|
||||||
final delta = node?.delta;
|
final delta = node?.delta;
|
||||||
if (node == null ||
|
if (node == null ||
|
||||||
|
@ -58,6 +54,8 @@ Future<bool> emojiCommandHandler(
|
||||||
if (previousCharacter != _emojiCharacter) return false;
|
if (previousCharacter != _emojiCharacter) return false;
|
||||||
if (!context.mounted) return false;
|
if (!context.mounted) return false;
|
||||||
|
|
||||||
|
if (!selection.isCollapsed) return false;
|
||||||
|
|
||||||
await editorState.insertTextAtPosition(
|
await editorState.insertTextAtPosition(
|
||||||
character,
|
character,
|
||||||
position: selection.start,
|
position: selection.start,
|
||||||
|
|
|
@ -174,11 +174,10 @@ class ExportTab extends StatelessWidget {
|
||||||
ClipboardServiceData(plainText: markdown),
|
ClipboardServiceData(plainText: markdown),
|
||||||
);
|
);
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
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) {
|
if (state.publishResult != null) {
|
||||||
state.publishResult!.fold(
|
state.publishResult!.fold(
|
||||||
(value) => showToastNotification(
|
(value) => showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_publishSuccessfully.tr(),
|
message: LocaleKeys.publish_publishSuccessfully.tr(),
|
||||||
),
|
),
|
||||||
(error) => showToastNotification(
|
(error) => showToastNotification(
|
||||||
context,
|
|
||||||
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
|
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
),
|
),
|
||||||
|
@ -97,11 +95,9 @@ class PublishTab extends StatelessWidget {
|
||||||
} else if (state.unpublishResult != null) {
|
} else if (state.unpublishResult != null) {
|
||||||
state.unpublishResult!.fold(
|
state.unpublishResult!.fold(
|
||||||
(value) => showToastNotification(
|
(value) => showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||||
),
|
),
|
||||||
(error) => showToastNotification(
|
(error) => showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_unpublishFailed.tr(),
|
message: LocaleKeys.publish_unpublishFailed.tr(),
|
||||||
description: error.msg,
|
description: error.msg,
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
|
@ -110,14 +106,12 @@ class PublishTab extends StatelessWidget {
|
||||||
} else if (state.updatePathNameResult != null) {
|
} else if (state.updatePathNameResult != null) {
|
||||||
state.updatePathNameResult!.fold(
|
state.updatePathNameResult!.fold(
|
||||||
(value) => showToastNotification(
|
(value) => showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||||
),
|
),
|
||||||
(error) {
|
(error) {
|
||||||
Log.error('update path name failed: $error');
|
Log.error('update path name failed: $error');
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),
|
message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
description: error.code.publishErrorMessage,
|
description: error.code.publishErrorMessage,
|
||||||
|
@ -182,7 +176,6 @@ class _PublishedWidgetState extends State<_PublishedWidget> {
|
||||||
);
|
);
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -292,7 +285,6 @@ class _PublishWidgetState extends State<_PublishWidget> {
|
||||||
// check if any database is selected
|
// check if any database is selected
|
||||||
if (_selectedViews.isEmpty) {
|
if (_selectedViews.isEmpty) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_noDatabaseSelected.tr(),
|
message: LocaleKeys.publish_noDatabaseSelected.tr(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -611,7 +603,6 @@ class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> {
|
||||||
// unable to deselect the primary database
|
// unable to deselect the primary database
|
||||||
if (isPrimaryDatabase) {
|
if (isPrimaryDatabase) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message:
|
message:
|
||||||
LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(),
|
LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -70,7 +70,6 @@ class ShareButton extends StatelessWidget {
|
||||||
case ShareType.html:
|
case ShareType.html:
|
||||||
case ShareType.csv:
|
case ShareType.csv:
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_files_exportFileSuccess.tr(),
|
message: LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -81,7 +80,6 @@ class ShareButton extends StatelessWidget {
|
||||||
|
|
||||||
void _handleExportError(BuildContext context, FlowyError error) {
|
void _handleExportError(BuildContext context, FlowyError error) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message:
|
message:
|
||||||
'${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}',
|
'${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}',
|
||||||
);
|
);
|
||||||
|
|
|
@ -117,7 +117,6 @@ class _ShareTabContent extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,6 @@ class _MobileSyncErrorPage extends StatelessWidget {
|
||||||
onTapUp: () {
|
onTapUp: () {
|
||||||
getIt<ClipboardService>().setPlainText(error.toString());
|
getIt<ClipboardService>().setPlainText(error.toString());
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
bottomPadding: 0,
|
bottomPadding: 0,
|
||||||
);
|
);
|
||||||
|
@ -101,7 +100,7 @@ class _DesktopSyncErrorPage extends StatelessWidget {
|
||||||
onTapUp: () {
|
onTapUp: () {
|
||||||
getIt<ClipboardService>().setPlainText(error.toString());
|
getIt<ClipboardService>().setPlainText(error.toString());
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
bottomPadding: 0,
|
bottomPadding: 0,
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,6 +27,7 @@ Future<String> customDocumentToMarkdown(
|
||||||
Document document, {
|
Document document, {
|
||||||
String path = '',
|
String path = '',
|
||||||
AsyncValueSetter<Archive>? onArchive,
|
AsyncValueSetter<Archive>? onArchive,
|
||||||
|
String lineBreak = '',
|
||||||
}) async {
|
}) async {
|
||||||
final List<Future<ArchiveFile>> fileFutures = [];
|
final List<Future<ArchiveFile>> fileFutures = [];
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ Future<String> customDocumentToMarkdown(
|
||||||
try {
|
try {
|
||||||
markdown = documentToMarkdown(
|
markdown = documentToMarkdown(
|
||||||
document,
|
document,
|
||||||
|
lineBreak: lineBreak,
|
||||||
customParsers: [
|
customParsers: [
|
||||||
const MathEquationNodeParser(),
|
const MathEquationNodeParser(),
|
||||||
const CalloutNodeParser(),
|
const CalloutNodeParser(),
|
||||||
|
|
|
@ -129,7 +129,7 @@ class AppFlowyCloudDeepLink {
|
||||||
final context = AppGlobals.rootNavKey.currentState?.context;
|
final context = AppGlobals.rootNavKey.currentState?.context;
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: err.msg,
|
message: err.msg,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,14 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) {
|
||||||
case ErrorCode.InvalidEncryptSecret:
|
case ErrorCode.InvalidEncryptSecret:
|
||||||
case ErrorCode.NetworkError:
|
case ErrorCode.NetworkError:
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: error.msg,
|
message: error.msg,
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: error.msg,
|
message: error.msg,
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
callbacks: ToastificationCallbacks(
|
callbacks: ToastificationCallbacks(
|
||||||
|
|
|
@ -65,7 +65,7 @@ class _SignInWithMagicLinkButtonsState
|
||||||
void _sendMagicLink(BuildContext context, String email) {
|
void _sendMagicLink(BuildContext context, String email) {
|
||||||
if (!isEmail(email)) {
|
if (!isEmail(email)) {
|
||||||
return showToastNotification(
|
return showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.signIn_invalidEmail.tr(),
|
message: LocaleKeys.signIn_invalidEmail.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,7 +25,7 @@ Future<void> shareLogFiles(BuildContext? context) async {
|
||||||
if (archiveLogFiles.isEmpty) {
|
if (archiveLogFiles.isEmpty) {
|
||||||
if (context != null && context.mounted) {
|
if (context != null && context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.noLogFiles.tr(),
|
message: LocaleKeys.noLogFiles.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
@ -42,7 +42,7 @@ Future<void> shareLogFiles(BuildContext? context) async {
|
||||||
if (zip == null) {
|
if (zip == null) {
|
||||||
if (context != null && context.mounted) {
|
if (context != null && context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.noLogFiles.tr(),
|
message: LocaleKeys.noLogFiles.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
@ -72,7 +72,7 @@ Future<void> shareLogFiles(BuildContext? context) async {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context != null && context.mounted) {
|
if (context != null && context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: e.toString(),
|
message: e.toString(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,11 +2,13 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy/core/config/kv.dart';
|
import 'package:appflowy/core/config/kv.dart';
|
||||||
import 'package:appflowy/core/config/kv_keys.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/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||||
import 'package:appflowy/util/color_to_hex_string.dart';
|
import 'package:appflowy/util/color_to_hex_string.dart';
|
||||||
import 'package:appflowy/workspace/application/appearance_defaults.dart';
|
import 'package:appflowy/workspace/application/appearance_defaults.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.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/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
|
|
||||||
part 'appearance_cubit.freezed.dart';
|
part 'appearance_cubit.freezed.dart';
|
||||||
|
|
||||||
|
@ -97,7 +100,19 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
|
||||||
Future<void> setTheme(String themeName) async {
|
Future<void> setTheme(String themeName) async {
|
||||||
_appearanceSettings.theme = themeName;
|
_appearanceSettings.theme = themeName;
|
||||||
unawaited(_saveAppearanceSettings());
|
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
|
/// Reset the current user selected theme back to the default
|
||||||
|
|
|
@ -169,7 +169,7 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> {
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: message,
|
message: message,
|
||||||
type: result.fold(
|
type: result.fold(
|
||||||
(_) => ToastificationType.success,
|
(_) => ToastificationType.success,
|
||||||
|
|
|
@ -193,7 +193,7 @@ Future<void> deleteMyAccount(
|
||||||
|
|
||||||
if (!isChecked) {
|
if (!isChecked) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.warning,
|
type: ToastificationType.warning,
|
||||||
bottomPadding: bottomPadding,
|
bottomPadding: bottomPadding,
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
|
@ -208,7 +208,7 @@ Future<void> deleteMyAccount(
|
||||||
|
|
||||||
if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) {
|
if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.warning,
|
type: ToastificationType.warning,
|
||||||
bottomPadding: bottomPadding,
|
bottomPadding: bottomPadding,
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
|
@ -226,7 +226,7 @@ Future<void> deleteMyAccount(
|
||||||
|
|
||||||
loading.stop();
|
loading.stop();
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.newSettings_myAccount_deleteAccount_deleteAccountSuccess
|
.newSettings_myAccount_deleteAccount_deleteAccountSuccess
|
||||||
.tr(),
|
.tr(),
|
||||||
|
@ -245,7 +245,7 @@ Future<void> deleteMyAccount(
|
||||||
|
|
||||||
loading.stop();
|
loading.stop();
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
bottomPadding: bottomPadding,
|
bottomPadding: bottomPadding,
|
||||||
message: f.msg,
|
message: f.msg,
|
||||||
|
|
|
@ -140,11 +140,9 @@ class LocalAISettingPanel extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const LocalAIStatusIndicator(),
|
const LocalAIStatusIndicator(),
|
||||||
if (state.showSettings) ...[
|
|
||||||
const VSpace(10),
|
const VSpace(10),
|
||||||
OllamaSettingPage(),
|
OllamaSettingPage(),
|
||||||
],
|
],
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -288,6 +288,10 @@ class _LackOfResource extends StatelessWidget {
|
||||||
text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(),
|
text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(),
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
),
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: modelNames.join(', '),
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' ',
|
text: ' ',
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
|
|
|
@ -157,7 +157,7 @@ class SettingsManageDataView extends StatelessWidget {
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.settings_manageDataPage_cache_dialog_successHint
|
.settings_manageDataPage_cache_dialog_successHint
|
||||||
.tr(),
|
.tr(),
|
||||||
|
|
|
@ -53,7 +53,6 @@ class SettingsPageSitesEvent {
|
||||||
);
|
);
|
||||||
getIt<ClipboardService>().setData(ClipboardServiceData(plainText: url));
|
getIt<ClipboardService>().setData(ClipboardServiceData(plainText: url));
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.message_copy_success.tr(),
|
message: LocaleKeys.message_copy_success.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -253,7 +253,6 @@ class _FreePlanUpgradeButton extends StatelessWidget {
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isOwner) {
|
if (isOwner) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message:
|
message:
|
||||||
LocaleKeys.settings_sites_namespace_redirectToPayment.tr(),
|
LocaleKeys.settings_sites_namespace_redirectToPayment.tr(),
|
||||||
type: ToastificationType.info,
|
type: ToastificationType.info,
|
||||||
|
@ -264,7 +263,6 @@ class _FreePlanUpgradeButton extends StatelessWidget {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.settings_sites_namespace_pleaseAskOwnerToSetHomePage
|
.settings_sites_namespace_pleaseAskOwnerToSetHomePage
|
||||||
.tr(),
|
.tr(),
|
||||||
|
|
|
@ -216,7 +216,6 @@ class _DomainSettingsDialogState extends State<DomainSettingsDialog> {
|
||||||
result.fold(
|
result.fold(
|
||||||
(s) {
|
(s) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(),
|
message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -234,7 +233,6 @@ class _DomainSettingsDialogState extends State<DomainSettingsDialog> {
|
||||||
Log.error('Failed to update namespace: $f');
|
Log.error('Failed to update namespace: $f');
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: basicErrorMessage,
|
message: basicErrorMessage,
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
description: errorMessage,
|
description: errorMessage,
|
||||||
|
|
|
@ -203,7 +203,6 @@ class _PublishedViewSettingsDialogState
|
||||||
result.fold(
|
result.fold(
|
||||||
(s) {
|
(s) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||||
);
|
);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@ -212,7 +211,6 @@ class _PublishedViewSettingsDialogState
|
||||||
Log.error('update path name failed: $f');
|
Log.error('update path name failed: $f');
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),
|
message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
description: f.code.publishErrorMessage,
|
description: f.code.publishErrorMessage,
|
||||||
|
|
|
@ -178,7 +178,6 @@ class _SettingsSitesPageView extends StatelessWidget {
|
||||||
Log.error('Failed to generate payment link for Pro Plan: ${f.msg}');
|
Log.error('Failed to generate payment link for Pro Plan: ${f.msg}');
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message:
|
message:
|
||||||
LocaleKeys.settings_sites_error_failedToGeneratePaymentLink.tr(),
|
LocaleKeys.settings_sites_error_failedToGeneratePaymentLink.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
|
@ -188,14 +187,12 @@ class _SettingsSitesPageView extends StatelessWidget {
|
||||||
result != null) {
|
result != null) {
|
||||||
result.fold((_) {
|
result.fold((_) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||||
);
|
);
|
||||||
}, (f) {
|
}, (f) {
|
||||||
Log.error('Failed to unpublish view: ${f.msg}');
|
Log.error('Failed to unpublish view: ${f.msg}');
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.publish_unpublishFailed.tr(),
|
message: LocaleKeys.publish_unpublishFailed.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
description: f.msg,
|
description: f.msg,
|
||||||
|
@ -204,14 +201,12 @@ class _SettingsSitesPageView extends StatelessWidget {
|
||||||
} else if (type == SettingsSitesActionType.setHomePage && result != null) {
|
} else if (type == SettingsSitesActionType.setHomePage && result != null) {
|
||||||
result.fold((s) {
|
result.fold((s) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_success_setHomepageSuccess.tr(),
|
message: LocaleKeys.settings_sites_success_setHomepageSuccess.tr(),
|
||||||
);
|
);
|
||||||
}, (f) {
|
}, (f) {
|
||||||
Log.error('Failed to set homepage: ${f.msg}');
|
Log.error('Failed to set homepage: ${f.msg}');
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_error_setHomepageFailed.tr(),
|
message: LocaleKeys.settings_sites_error_setHomepageFailed.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
@ -220,14 +215,12 @@ class _SettingsSitesPageView extends StatelessWidget {
|
||||||
result != null) {
|
result != null) {
|
||||||
result.fold((s) {
|
result.fold((s) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_success_removeHomePageSuccess.tr(),
|
message: LocaleKeys.settings_sites_success_removeHomePageSuccess.tr(),
|
||||||
);
|
);
|
||||||
}, (f) {
|
}, (f) {
|
||||||
Log.error('Failed to remove homepage: ${f.msg}');
|
Log.error('Failed to remove homepage: ${f.msg}');
|
||||||
|
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_sites_error_removeHomePageFailed.tr(),
|
message: LocaleKeys.settings_sites_error_removeHomePageFailed.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
|
|
@ -363,7 +363,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> {
|
||||||
}) async {
|
}) async {
|
||||||
if (cloudUrl.isEmpty || webUrl.isEmpty) {
|
if (cloudUrl.isEmpty || webUrl.isEmpty) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(),
|
message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
@ -375,7 +374,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_menu_changeUrl.tr(args: [cloudUrl]),
|
message: LocaleKeys.settings_menu_changeUrl.tr(args: [cloudUrl]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -387,7 +385,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> {
|
||||||
await runAppFlowy();
|
await runAppFlowy();
|
||||||
} else {
|
} else {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(),
|
message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(),
|
||||||
type: ToastificationType.error,
|
type: ToastificationType.error,
|
||||||
);
|
);
|
||||||
|
@ -522,7 +519,6 @@ class _SupportSettings extends StatelessWidget {
|
||||||
await getIt<FlowyCacheManager>().clearAllCache();
|
await getIt<FlowyCacheManager>().clearAllCache();
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys
|
message: LocaleKeys
|
||||||
.settings_manageDataPage_cache_dialog_successHint
|
.settings_manageDataPage_cache_dialog_successHint
|
||||||
.tr(),
|
.tr(),
|
||||||
|
|
|
@ -71,7 +71,7 @@ class AppFlowyCloudViewSetting extends StatelessWidget {
|
||||||
const VSpace(8),
|
const VSpace(8),
|
||||||
const AppFlowyCloudEnableSync(),
|
const AppFlowyCloudEnableSync(),
|
||||||
const VSpace(6),
|
const VSpace(6),
|
||||||
const AppFlowyCloudSyncLogEnabled(),
|
// const AppFlowyCloudSyncLogEnabled(),
|
||||||
const VSpace(12),
|
const VSpace(12),
|
||||||
RestartButton(
|
RestartButton(
|
||||||
onClick: () {
|
onClick: () {
|
||||||
|
|
|
@ -157,7 +157,6 @@ class _NavigatorTextFieldDialogState extends State<NavigatorTextFieldDialog> {
|
||||||
onOkPressed: () {
|
onOkPressed: () {
|
||||||
if (newValue.isEmpty) {
|
if (newValue.isEmpty) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(),
|
message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -363,8 +362,7 @@ class OkCancelButton extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showToastNotification(
|
void showToastNotification({
|
||||||
BuildContext context, {
|
|
||||||
String? message,
|
String? message,
|
||||||
TextSpan? richMessage,
|
TextSpan? richMessage,
|
||||||
String? description,
|
String? description,
|
||||||
|
|
|
@ -51,7 +51,6 @@ class FlowyVersionSection extends CustomActionCell {
|
||||||
}
|
}
|
||||||
enableDocumentInternalLog = !enableDocumentInternalLog;
|
enableDocumentInternalLog = !enableDocumentInternalLog;
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: enableDocumentInternalLog
|
message: enableDocumentInternalLog
|
||||||
? 'Enabled Internal Log'
|
? 'Enabled Internal Log'
|
||||||
: 'Disabled Internal Log',
|
: 'Disabled Internal Log',
|
||||||
|
|
|
@ -74,7 +74,6 @@ class ViewTitleBar extends StatelessWidget {
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.isLocked) {
|
if (state.isLocked) {
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
context,
|
|
||||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,8 +98,8 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "552f95f"
|
ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
|
||||||
resolved-ref: "552f95fd15627e10a138c6db2a6d0a8089bc9a25"
|
resolved-ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "5.1.0"
|
version: "5.1.0"
|
||||||
|
|
|
@ -2,13 +2,13 @@ name: appflowy
|
||||||
description: Bring projects, wikis, and teams together with AI. AppFlowy is an
|
description: Bring projects, wikis, and teams together with AI. AppFlowy is an
|
||||||
AI collaborative workspace where you achieve more without losing control of
|
AI collaborative workspace where you achieve more without losing control of
|
||||||
your data. The best open source alternative to Notion.
|
your data. The best open source alternative to Notion.
|
||||||
publish_to: "none"
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.8.8
|
version: 0.8.9
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
flutter: ">=3.27.4"
|
flutter: '>=3.27.4'
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
any_date: ^1.0.4
|
any_date: ^1.0.4
|
||||||
|
@ -41,7 +41,7 @@ dependencies:
|
||||||
calendar_view:
|
calendar_view:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Xazin/flutter_calendar_view
|
url: https://github.com/Xazin/flutter_calendar_view
|
||||||
ref: "6fe0c98"
|
ref: '6fe0c98'
|
||||||
collection: ^1.17.1
|
collection: ^1.17.1
|
||||||
connectivity_plus: ^5.0.2
|
connectivity_plus: ^5.0.2
|
||||||
cross_file: ^0.3.4+1
|
cross_file: ^0.3.4+1
|
||||||
|
@ -77,7 +77,7 @@ dependencies:
|
||||||
flutter_emoji_mart:
|
flutter_emoji_mart:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/LucasXu0/emoji_mart.git
|
url: https://github.com/LucasXu0/emoji_mart.git
|
||||||
ref: "355aa56"
|
ref: '355aa56'
|
||||||
flutter_math_fork: ^0.7.3
|
flutter_math_fork: ^0.7.3
|
||||||
flutter_slidable: ^3.0.0
|
flutter_slidable: ^3.0.0
|
||||||
|
|
||||||
|
@ -187,13 +187,13 @@ dependency_overrides:
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: "552f95f"
|
ref: '361b99c38370abeeb19656f89e8c31cb3666623b'
|
||||||
|
|
||||||
appflowy_editor_plugins:
|
appflowy_editor_plugins:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git
|
url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git
|
||||||
path: "packages/appflowy_editor_plugins"
|
path: 'packages/appflowy_editor_plugins'
|
||||||
ref: "4efcff7"
|
ref: '4efcff7'
|
||||||
|
|
||||||
sheet:
|
sheet:
|
||||||
git:
|
git:
|
||||||
|
|
|
@ -375,7 +375,7 @@ void main() {
|
||||||
await blocResponseFuture();
|
await blocResponseFuture();
|
||||||
bloc.runResponseAction(SuggestionAction.accept);
|
bloc.runResponseAction(SuggestionAction.accept);
|
||||||
await blocResponseFuture();
|
await blocResponseFuture();
|
||||||
expect(editorState.document.root.children.length, 1);
|
expect(editorState.document.root.children.length, 2);
|
||||||
expect(
|
expect(
|
||||||
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
|
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
|
||||||
'Hello World',
|
'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
|
const _sample1 = '''# The Curious Cat
|
||||||
|
|
|
@ -628,7 +628,8 @@
|
||||||
"theme": {
|
"theme": {
|
||||||
"title": "Theme",
|
"title": "Theme",
|
||||||
"description": "Select a preset theme, or upload your own custom 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": {
|
"workspaceFont": {
|
||||||
"title": "Workspace font",
|
"title": "Workspace font",
|
||||||
|
@ -884,7 +885,7 @@
|
||||||
"pleaseFollowThese": "Please follow these",
|
"pleaseFollowThese": "Please follow these",
|
||||||
"instructions": "instructions",
|
"instructions": "instructions",
|
||||||
"installOllamaLai": "to set up Ollama and AppFlowy Local AI.",
|
"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."
|
"downloadModel": "to download them."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
10
frontend/rust-lib/Cargo.lock
generated
10
frontend/rust-lib/Cargo.lock
generated
|
@ -345,12 +345,11 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "af-local-ai"
|
name = "af-local-ai"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"af-plugin",
|
"af-plugin",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -358,14 +357,12 @@ dependencies = [
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"zip 2.2.0",
|
|
||||||
"zip-extensions",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "af-mcp"
|
name = "af-mcp"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -379,7 +376,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "af-plugin"
|
name = "af-plugin"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
@ -2504,7 +2501,6 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
"validator 0.18.1",
|
"validator 0.18.1",
|
||||||
"winreg 0.55.0",
|
|
||||||
"zip 2.2.0",
|
"zip 2.2.0",
|
||||||
"zip-extensions",
|
"zip-extensions",
|
||||||
]
|
]
|
||||||
|
|
|
@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl
|
||||||
# To update the commit ID, run:
|
# To update the commit ID, run:
|
||||||
# scripts/tool/update_local_ai_rev.sh new_rev_id
|
# 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-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 = "4b3d50cbec2f58be2ac385231b8f585f1555e282" }
|
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 = "4b3d50cbec2f58be2ac385231b8f585f1555e282" }
|
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"
|
notify = "6.1.1"
|
||||||
af-mcp = { version = "0.1.0" }
|
af-mcp = { version = "0.1.0" }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
|
||||||
winreg = "0.55"
|
|
||||||
|
|
||||||
#cmd_lib = { version = "1.9.5" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
|
@ -13,9 +13,9 @@ use futures::Sink;
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::local_ai::watch::is_plugin_ready;
|
|
||||||
use crate::stream_message::StreamMessage;
|
use crate::stream_message::StreamMessage;
|
||||||
use af_local_ai::ollama_plugin::OllamaAIPlugin;
|
use af_local_ai::ollama_plugin::OllamaAIPlugin;
|
||||||
|
use af_plugin::core::path::is_plugin_ready;
|
||||||
use af_plugin::core::plugin::RunningState;
|
use af_plugin::core::plugin::RunningState;
|
||||||
use arc_swap::ArcSwapOption;
|
use arc_swap::ArcSwapOption;
|
||||||
use futures_util::SinkExt;
|
use futures_util::SinkExt;
|
||||||
|
|
|
@ -5,13 +5,13 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
|
|
||||||
use crate::entities::LackOfAIResourcePB;
|
use crate::entities::LackOfAIResourcePB;
|
||||||
use crate::local_ai::watch::{is_plugin_ready, ollama_plugin_path};
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::local_ai::watch::{watch_offline_app, WatchContext};
|
use crate::local_ai::watch::{watch_offline_app, WatchContext};
|
||||||
use crate::notification::{
|
use crate::notification::{
|
||||||
chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY,
|
chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY,
|
||||||
};
|
};
|
||||||
use af_local_ai::ollama_plugin::OllamaPluginConfig;
|
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 lib_infra::util::{get_operating_system, OperatingSystem};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -195,9 +195,14 @@ impl LocalAIResourceController {
|
||||||
let tags: TagsResponse = resp.json().await.inspect_err(|e| {
|
let tags: TagsResponse = resp.json().await.inspect_err(|e| {
|
||||||
log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {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 {
|
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!(
|
log::trace!(
|
||||||
"[LLM Resource] required model '{}' not found in API response",
|
"[LLM Resource] required model '{}' not found in API response",
|
||||||
required
|
required
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
use crate::local_ai::resource::WatchDiskEvent;
|
use crate::local_ai::resource::WatchDiskEvent;
|
||||||
|
use af_plugin::core::path::{install_path, ollama_plugin_path};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
||||||
use tracing::{error, trace};
|
use tracing::{error, trace};
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use winreg::{enums::*, RegKey};
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct WatchContext {
|
pub struct WatchContext {
|
||||||
|
@ -61,131 +58,3 @@ pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver<Watch
|
||||||
rx,
|
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