mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 06:37:14 -04:00
Merge branch 'main' into sign_in_with_password_or_passcode
This commit is contained in:
commit
0344afddf4
239 changed files with 9903 additions and 1882 deletions
|
@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
|||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.8.8"
|
||||
APPFLOWY_VERSION = "0.8.9"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
|
3210
frontend/appflowy_flutter/assets/translations/mr-IN.json
Normal file
3210
frontend/appflowy_flutter/assets/translations/mr-IN.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -15,6 +15,7 @@ void main() {
|
|||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a database and add a linked database view
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
|
||||
|
||||
|
@ -29,6 +30,11 @@ void main() {
|
|||
await tester.tapHidePropertyButton();
|
||||
tester.noFieldWithName('New field 1');
|
||||
|
||||
// create another field, New field 1 to be hidden still
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.dismissFieldEditor();
|
||||
tester.noFieldWithName('New field 1');
|
||||
|
||||
// go back to inline database view, expect field to be shown
|
||||
await tester.tapTabBarLinkedViewByViewName('Untitled');
|
||||
tester.findFieldWithName('New field 1');
|
||||
|
@ -60,5 +66,40 @@ void main() {
|
|||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1");
|
||||
});
|
||||
|
||||
testWidgets('field cell width', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a database and add a linked database view
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// check the width of the field
|
||||
expect(tester.getFieldWidth('New field 1'), 150);
|
||||
|
||||
// change the width of the field
|
||||
await tester.changeFieldWidth('New field 1', 200);
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
|
||||
// create another field, New field 1 to be same width
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.dismissFieldEditor();
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
|
||||
// go back to inline database view, expect New field 1 to be 150px
|
||||
await tester.tapTabBarLinkedViewByViewName('Untitled');
|
||||
expect(tester.getFieldWidth('New field 1'), 150);
|
||||
|
||||
// go back to linked database view, expect New field 1 to be 205px
|
||||
await tester.tapTabBarLinkedViewByViewName('Grid');
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
|
@ -47,5 +48,41 @@ void main() {
|
|||
|
||||
expect(editorState.selection!.start.offset, 0);
|
||||
});
|
||||
|
||||
testWidgets('select and delete text', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
/// input text
|
||||
final editor = tester.editor;
|
||||
final editorState = editor.getCurrentEditorState();
|
||||
|
||||
const inputText = 'Test for text selection and deletion';
|
||||
final texts = inputText.split(' ');
|
||||
await editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(inputText);
|
||||
|
||||
/// selecte and delete
|
||||
int index = 0;
|
||||
while (texts.isNotEmpty) {
|
||||
final text = texts.removeAt(0);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: index),
|
||||
end: Position(path: [0], offset: index + text.length),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
|
||||
index++;
|
||||
}
|
||||
|
||||
/// excpete the text value is correct
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
final nodeText = node.delta?.toPlainText() ?? '';
|
||||
expect(nodeText, ' ' * (index - 1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -942,6 +942,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
Future<void> changeFieldWidth(String fieldName, double width) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
|
||||
);
|
||||
await hoverOnWidget(
|
||||
field,
|
||||
onHover: () async {
|
||||
final dragHandle = find.descendant(
|
||||
of: field,
|
||||
matching: find.byType(DragToExpandLine),
|
||||
);
|
||||
await drag(dragHandle, Offset(width - getSize(field).width, 0));
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double getFieldWidth(String fieldName) {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
|
||||
);
|
||||
|
||||
return getSize(field).width;
|
||||
}
|
||||
|
||||
Future<void> findDateEditor(dynamic matcher) async {
|
||||
final finder = find.byType(DateCellEditor);
|
||||
expect(finder, matcher);
|
||||
|
|
|
@ -133,7 +133,6 @@ Future<bool> _afLaunchLocalUri(
|
|||
};
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: message,
|
||||
type: result.type == ResultType.done
|
||||
? ToastificationType.success
|
||||
|
|
|
@ -336,7 +336,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
|||
listener: (context, state) {
|
||||
if (state.isLocked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||
);
|
||||
|
||||
|
@ -366,7 +365,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
|||
listener: (context, state) {
|
||||
if (state.isLocked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -161,7 +161,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
context.pop();
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_duplicateSuccessfully.tr(),
|
||||
);
|
||||
}
|
||||
|
@ -170,7 +169,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
_toggleFavorite(context);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_favoriteSuccessfully.tr(),
|
||||
);
|
||||
}
|
||||
|
@ -179,7 +177,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
_toggleFavorite(context);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
||||
);
|
||||
}
|
||||
|
@ -202,8 +199,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copy.tr(),
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -234,12 +230,10 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||
);
|
||||
} else {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
@ -323,11 +317,9 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
if (state.publishResult != null) {
|
||||
state.publishResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_publishSuccessfully.tr(),
|
||||
),
|
||||
(error) => showToastNotification(
|
||||
context,
|
||||
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
|
||||
type: ToastificationType.error,
|
||||
),
|
||||
|
@ -335,11 +327,9 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
} else if (state.unpublishResult != null) {
|
||||
state.unpublishResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||
),
|
||||
(error) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishFailed.tr(),
|
||||
description: error.msg,
|
||||
type: ToastificationType.error,
|
||||
|
@ -349,7 +339,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
state.updatePathNameResult!.onSuccess(
|
||||
(value) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||
);
|
||||
|
|
|
@ -65,7 +65,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
|||
Navigator.pop(context);
|
||||
context.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_duplicateSuccessfully.tr(),
|
||||
);
|
||||
break;
|
||||
|
@ -84,7 +83,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
|||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(widget.view));
|
||||
showToastNotification(
|
||||
context,
|
||||
message: !widget.view.isFavorite
|
||||
? LocaleKeys.button_favoriteSuccessfully.tr()
|
||||
: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
||||
|
@ -146,7 +144,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
|||
Navigator.pop(context);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.sideBar_removeSuccess.tr(),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -45,7 +45,6 @@ enum MobilePaneActionType {
|
|||
size: 24.0,
|
||||
onPressed: (context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
||||
);
|
||||
|
||||
|
@ -61,7 +60,6 @@ enum MobilePaneActionType {
|
|||
size: 24.0,
|
||||
onPressed: (context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_favoriteSuccessfully.tr(),
|
||||
);
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
|
|||
Log.info('Open row page(${widget.documentId})');
|
||||
|
||||
if (view == null) {
|
||||
showToastNotification(context, message: 'Failed to open row page');
|
||||
showToastNotification(message: 'Failed to open row page');
|
||||
// reload the view again
|
||||
unawaited(_preloadView(context));
|
||||
Log.error('Failed to open row page(${widget.documentId})');
|
||||
|
|
|
@ -329,7 +329,7 @@ class _HomePageState extends State<_HomePage> {
|
|||
}
|
||||
|
||||
if (message != null) {
|
||||
showToastNotification(context, message: message, type: toastType);
|
||||
showToastNotification(message: message, type: toastType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -339,7 +339,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
|||
context.read<SpaceBloc>().add(const SpaceEvent.duplicate());
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_success_duplicateSpace.tr(),
|
||||
);
|
||||
|
||||
|
@ -374,7 +373,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
|||
.add(SpaceEvent.rename(space: widget.space, name: name));
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_success_renameSpace.tr(),
|
||||
);
|
||||
},
|
||||
|
@ -424,7 +422,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
|||
);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_success_updateSpace.tr(),
|
||||
);
|
||||
|
||||
|
@ -457,7 +454,6 @@ class _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {
|
|||
context.read<SpaceBloc>().add(SpaceEvent.delete(widget.space));
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_success_deleteSpace.tr(),
|
||||
);
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ class MobileInlineActionsWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasIcon = item.icon != null;
|
||||
final hasIcon = item.iconBuilder != null;
|
||||
return Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
|
@ -119,7 +119,7 @@ class MobileInlineActionsWidget extends StatelessWidget {
|
|||
child: Row(
|
||||
children: [
|
||||
if (hasIcon) ...[
|
||||
item.icon!.call(isSelected),
|
||||
item.iconBuilder!.call(isSelected),
|
||||
SizedBox(width: 12),
|
||||
],
|
||||
Flexible(
|
||||
|
|
|
@ -332,7 +332,6 @@ class _NotificationNavigationBar extends StatelessWidget {
|
|||
}
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys
|
||||
.settings_notifications_markAsReadNotifications_allSuccess
|
||||
.tr(),
|
||||
|
@ -350,7 +349,6 @@ class _NotificationNavigationBar extends StatelessWidget {
|
|||
}
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
|
||||
.tr(),
|
||||
);
|
||||
|
|
|
@ -108,7 +108,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
|||
|
||||
void _onMarkAllAsRead(BuildContext context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys
|
||||
.settings_notifications_markAsReadNotifications_allSuccess
|
||||
.tr(),
|
||||
|
@ -119,7 +118,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
|||
|
||||
void _onArchiveAll(BuildContext context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
|
||||
.tr(),
|
||||
);
|
||||
|
@ -133,7 +131,6 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
|||
}
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: 'Unarchive all success (Debug Mode)',
|
||||
);
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ enum NotificationPaneActionType {
|
|||
size: 24.0,
|
||||
onPressed: (context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys
|
||||
.settings_notifications_markAsReadNotifications_success
|
||||
.tr(),
|
||||
|
@ -55,7 +54,6 @@ enum NotificationPaneActionType {
|
|||
size: 24.0,
|
||||
onPressed: (context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: 'Unarchive notification success',
|
||||
);
|
||||
|
||||
|
@ -168,7 +166,6 @@ class _NotificationMoreActions extends StatelessWidget {
|
|||
Navigator.of(context).pop();
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_notifications_markAsReadNotifications_success
|
||||
.tr(),
|
||||
);
|
||||
|
@ -191,7 +188,6 @@ class _NotificationMoreActions extends StatelessWidget {
|
|||
|
||||
void _onArchive(BuildContext context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_notifications_archiveNotifications_success
|
||||
.tr()
|
||||
.tr(),
|
||||
|
|
|
@ -74,7 +74,6 @@ class _NotificationTabState extends State<NotificationTab>
|
|||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_notifications_refreshSuccess.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -81,7 +81,6 @@ class SupportSettingGroup extends StatelessWidget {
|
|||
);
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -201,7 +201,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
|
@ -218,7 +217,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||
});
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
bottomPadding: keyboardHeight,
|
||||
message: message,
|
||||
|
@ -229,7 +227,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
|
@ -247,7 +244,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||
});
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: message,
|
||||
bottomPadding: keyboardHeight,
|
||||
|
@ -258,7 +254,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_removeFromWorkspaceSuccess
|
||||
.tr(),
|
||||
|
@ -267,7 +262,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
},
|
||||
(f) {
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_removeFromWorkspaceFailed
|
||||
|
@ -283,7 +277,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
|||
final email = emailController.text;
|
||||
if (!isEmail(email)) {
|
||||
return showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
|
||||
);
|
||||
|
|
|
@ -184,8 +184,7 @@ class CopyButton extends StatelessWidget {
|
|||
);
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copiedNotification.tr(),
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -376,8 +376,7 @@ class ChatAIMessagePopup extends StatelessWidget {
|
|||
}
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copiedNotification.tr(),
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -14,7 +14,6 @@ import 'package:universal_platform/universal_platform.dart';
|
|||
void openPageFromMessage(BuildContext context, ViewPB? view) {
|
||||
if (view == null) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
@ -36,7 +35,6 @@ void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
|
|||
return;
|
||||
}
|
||||
showToastNotification(
|
||||
context,
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
|
|
|
@ -241,6 +241,11 @@ class SelectOptionCellEditorBloc
|
|||
} else if (!state.selectedOptions
|
||||
.any((option) => option.id == focusedOptionId)) {
|
||||
_selectOptionService.select(optionIds: [focusedOptionId]);
|
||||
emit(
|
||||
state.copyWith(
|
||||
clearFilter: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -411,23 +411,28 @@ class FieldController {
|
|||
/// Listen for field setting changes in the backend.
|
||||
void _listenOnFieldSettingsChanged() {
|
||||
FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
var updatedField = newFields.firstOrNull;
|
||||
final newFields = [...fieldInfos];
|
||||
|
||||
if (updatedField == null) {
|
||||
if (newFields.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final index = newFields
|
||||
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
|
||||
|
||||
if (index != -1) {
|
||||
newFields[index] =
|
||||
newFields[index].copyWith(fieldSettings: updatedFieldSettings);
|
||||
updatedField = newFields[index];
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
_fieldSettings
|
||||
..removeWhere(
|
||||
(field) => field.fieldId == updatedFieldSettings.fieldId,
|
||||
)
|
||||
..add(updatedFieldSettings);
|
||||
return newFields[index];
|
||||
}
|
||||
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
return updatedField;
|
||||
return null;
|
||||
}
|
||||
|
||||
_fieldSettingsListener.start(
|
||||
|
|
|
@ -108,7 +108,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
|
|||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: _DragToExpandLine(),
|
||||
child: DragToExpandLine(),
|
||||
);
|
||||
|
||||
return _GridHeaderCellContainer(
|
||||
|
@ -158,8 +158,11 @@ class _GridHeaderCellContainer extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _DragToExpandLine extends StatelessWidget {
|
||||
const _DragToExpandLine();
|
||||
@visibleForTesting
|
||||
class DragToExpandLine extends StatelessWidget {
|
||||
const DragToExpandLine({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -203,7 +203,7 @@ class MobileURLEditor extends StatelessWidget {
|
|||
ClipboardData(text: textEditingController.text),
|
||||
);
|
||||
Fluttertoast.showToast(
|
||||
msg: LocaleKeys.grid_url_copiedNotification.tr(),
|
||||
msg: LocaleKeys.message_copy_success.tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
context.pop();
|
||||
|
|
|
@ -442,7 +442,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||
final context = AppGlobals.rootNavKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: 'document integrity check failed',
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
|
|
@ -150,7 +150,6 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
|
|||
});
|
||||
} else {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||
);
|
||||
}
|
||||
|
@ -196,7 +195,6 @@ class ImproveWritingButton extends StatelessWidget {
|
|||
_insertAiNode(editorState, AiWriterCommand.improveWriting);
|
||||
} else {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
@ -15,6 +16,11 @@ import 'ai_writer_block_operations.dart';
|
|||
import 'ai_writer_entities.dart';
|
||||
import 'ai_writer_node_extension.dart';
|
||||
|
||||
/// Enable the debug log for the AiWriterCubit.
|
||||
///
|
||||
/// This is useful for debugging the AI writer cubit.
|
||||
const _aiWriterCubitDebugLog = false;
|
||||
|
||||
class AiWriterCubit extends Cubit<AiWriterState> {
|
||||
AiWriterCubit({
|
||||
required this.documentId,
|
||||
|
@ -95,6 +101,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
final command = node.aiWriterCommand;
|
||||
final (run, prompt) = await _addSelectionTextToRecords(command);
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'command: $command, run: $run, prompt: $prompt',
|
||||
);
|
||||
|
||||
if (!run) {
|
||||
await exit();
|
||||
return;
|
||||
|
@ -211,20 +221,26 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Accept
|
||||
//
|
||||
// If the user clicks accept, we need to replace the selection with the AI's response
|
||||
if (action case SuggestionAction.accept) {
|
||||
await _textRobot.persist();
|
||||
await formatSelection(
|
||||
editorState,
|
||||
selection,
|
||||
ApplySuggestionFormatType.clear,
|
||||
// trim the markdown text to avoid extra new lines
|
||||
final trimmedMarkdownText = _textRobot.markdownText.trim();
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'trigger accept action, markdown text: $trimmedMarkdownText',
|
||||
);
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
final transaction = editorState.transaction..deleteNodes(nodes);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
withUpdateSelection: false,
|
||||
|
||||
await _textRobot.deleteAINodes();
|
||||
|
||||
await _textRobot.replace(
|
||||
selection: selection,
|
||||
markdownText: trimmedMarkdownText,
|
||||
);
|
||||
|
||||
await exit(withDiscard: false, withUnformat: false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -276,17 +292,24 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
AiWriterCommand command,
|
||||
) async {
|
||||
final node = aiWriterNode;
|
||||
|
||||
// check the node is registered
|
||||
if (node == null) {
|
||||
return (false, '');
|
||||
}
|
||||
|
||||
// check the selection is valid
|
||||
final selection = node.aiWriterSelection?.normalized;
|
||||
if (selection == null) {
|
||||
return (false, '');
|
||||
}
|
||||
|
||||
// if the command is continue writing, we don't need to get the selection text
|
||||
if (command == AiWriterCommand.continueWriting) {
|
||||
return (true, '');
|
||||
}
|
||||
|
||||
// if the selection is collapsed, we don't need to get the selection text
|
||||
if (selection.isCollapsed) {
|
||||
return (true, '');
|
||||
}
|
||||
|
@ -297,6 +320,7 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
records.add(
|
||||
AiWriterRecord.user(content: selectionText, format: null),
|
||||
);
|
||||
|
||||
return (true, '');
|
||||
} else {
|
||||
return (true, selectionText);
|
||||
|
@ -540,6 +564,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
attributes: ApplySuggestionFormatType.replace.attributes,
|
||||
);
|
||||
onAppendToDocument?.call();
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'received message: $text',
|
||||
);
|
||||
},
|
||||
processAssistMessage: (text) async {
|
||||
if (state case final GeneratingAiWriterState generatingState) {
|
||||
|
@ -551,6 +579,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'received assist message: $text',
|
||||
);
|
||||
},
|
||||
onEnd: () async {
|
||||
if (state case final GeneratingAiWriterState generatingState) {
|
||||
|
@ -567,6 +599,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
records.add(
|
||||
AiWriterRecord.ai(content: _textRobot.markdownText),
|
||||
);
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'returned response: ${_textRobot.markdownText}',
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) async {
|
||||
|
@ -658,6 +694,12 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _aiWriterCubitLog(String message) {
|
||||
if (_aiWriterCubitDebugLog) {
|
||||
Log.debug('[AiWriterCubit] $message');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mixin RegisteredAiWriter {
|
||||
|
|
|
@ -57,11 +57,30 @@ extension AiWriterNodeExtension on EditorState {
|
|||
slicedNodes.add(copiedNode);
|
||||
}
|
||||
|
||||
for (final (i, node) in slicedNodes.indexed) {
|
||||
final childNodesShouldBeDeleted = <Node>[];
|
||||
for (final child in node.children) {
|
||||
if (!child.path.inSelection(selection)) {
|
||||
childNodesShouldBeDeleted.add(child);
|
||||
}
|
||||
}
|
||||
for (final child in childNodesShouldBeDeleted) {
|
||||
slicedNodes[i] = node.copyWith(
|
||||
children: node.children.where((e) => e.id != child.id).toList(),
|
||||
type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// use \n\n as line break to improve the ai response
|
||||
// using \n will cause the ai response treat the text as a single line
|
||||
final markdown = await customDocumentToMarkdown(
|
||||
Document.blank()..insert([0], slicedNodes),
|
||||
lineBreak: '\n\n',
|
||||
);
|
||||
|
||||
return markdown;
|
||||
// trim the last \n if it exists
|
||||
return markdown.trimRight();
|
||||
}
|
||||
|
||||
List<String> getPlainTextInSelection(Selection? selection) {
|
||||
|
|
|
@ -70,13 +70,12 @@ extension InsertDatabase on EditorState {
|
|||
node,
|
||||
selection.end.offset,
|
||||
0,
|
||||
r'$',
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.page.name,
|
||||
MentionBlockKeys.pageId: view.id,
|
||||
},
|
||||
},
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: MentionBlockKeys.buildMentionPageAttributes(
|
||||
mentionType: MentionType.page,
|
||||
pageId: view.id,
|
||||
blockId: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -110,10 +110,13 @@ class MarkdownTextRobot {
|
|||
}
|
||||
|
||||
/// Persist the text into the document
|
||||
Future<void> persist({String? markdownText}) async {
|
||||
Future<void> persist({
|
||||
String? markdownText,
|
||||
}) async {
|
||||
if (markdownText != null) {
|
||||
_markdownText = markdownText;
|
||||
}
|
||||
|
||||
await _lock.synchronized(() async {
|
||||
await _refresh(inMemoryUpdate: false);
|
||||
});
|
||||
|
@ -124,6 +127,34 @@ class MarkdownTextRobot {
|
|||
}
|
||||
}
|
||||
|
||||
/// Replace the selected content with the AI's response
|
||||
Future<void> replace({
|
||||
required Selection selection,
|
||||
required String markdownText,
|
||||
}) async {
|
||||
if (selection.isSingle) {
|
||||
await _replaceInSameLine(
|
||||
selection: selection,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
} else {
|
||||
await _replaceInMultiLines(
|
||||
selection: selection,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the temporary inserted AI nodes
|
||||
Future<void> deleteAINodes() async {
|
||||
final nodes = getInsertedNodes();
|
||||
final transaction = editorState.transaction..deleteNodes(nodes);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(recordUndo: false),
|
||||
);
|
||||
}
|
||||
|
||||
/// Discard the inserted content
|
||||
Future<void> discard() async {
|
||||
final start = _insertPosition;
|
||||
|
@ -282,6 +313,161 @@ class MarkdownTextRobot {
|
|||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
/// If the selected content is in the same line,
|
||||
/// keep the selected node and replace the delta.
|
||||
Future<void> _replaceInSameLine({
|
||||
required Selection selection,
|
||||
required String markdownText,
|
||||
}) async {
|
||||
selection = selection.normalized;
|
||||
|
||||
// If the selection is not a single node, do nothing.
|
||||
if (!selection.isSingle) {
|
||||
assert(false, 'Expected single node selection');
|
||||
Log.error('Expected single node selection');
|
||||
return;
|
||||
}
|
||||
|
||||
final startIndex = selection.startIndex;
|
||||
final endIndex = selection.endIndex;
|
||||
final length = endIndex - startIndex;
|
||||
|
||||
// Get the selected node.
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
assert(false, 'Expected non-null node and delta');
|
||||
Log.error('Expected non-null node and delta');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the markdown text to delta.
|
||||
// Question: Why we need to convert the markdown to document first?
|
||||
// Answer: Because the markdown text may contain the list item,
|
||||
// if we convert the markdown to delta directly, the list item will be
|
||||
// treated as a normal text node, and the delta will be incorrect.
|
||||
// For example, the markdown text is:
|
||||
// ```
|
||||
// 1. item1
|
||||
// ```
|
||||
// if we convert the markdown to delta directly, the delta will be:
|
||||
// ```
|
||||
// [
|
||||
// {
|
||||
// "insert": "1. item1"
|
||||
// }
|
||||
// ]
|
||||
// ```
|
||||
// if we convert the markdown to document first, the document will be:
|
||||
// ```
|
||||
// [
|
||||
// {
|
||||
// "type": "numbered_list",
|
||||
// "children": [
|
||||
// {
|
||||
// "insert": "item1"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
final document = customMarkdownToDocument(markdownText);
|
||||
final decoder = DeltaMarkdownDecoder();
|
||||
final markdownDelta =
|
||||
document.nodeAtPath([0])?.delta ?? decoder.convert(markdownText);
|
||||
|
||||
// Replace the delta of the selected node.
|
||||
final transaction = editorState.transaction;
|
||||
transaction
|
||||
..deleteText(node, startIndex, length)
|
||||
..insertTextDelta(node, startIndex, markdownDelta);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
/// If the selected content is in multiple lines
|
||||
Future<void> _replaceInMultiLines({
|
||||
required Selection selection,
|
||||
required String markdownText,
|
||||
}) async {
|
||||
selection = selection.normalized;
|
||||
|
||||
// If the selection is a single node, do nothing.
|
||||
if (selection.isSingle) {
|
||||
assert(false, 'Expected multi-line selection');
|
||||
Log.error('Expected multi-line selection');
|
||||
return;
|
||||
}
|
||||
|
||||
final markdownNodes = customMarkdownToDocument(
|
||||
markdownText,
|
||||
tableWidth: 250.0,
|
||||
).root.children;
|
||||
|
||||
// Get the selected nodes.
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
|
||||
// Note: Don't change its order, otherwise the delta will be incorrect.
|
||||
// step 1. merge the first selected node and the first node from the ai response
|
||||
// step 2. merge the last selected node and the last node from the ai response
|
||||
// step 3. insert the middle nodes from the ai response
|
||||
// step 4. delete the middle nodes
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
// step 1
|
||||
final firstNode = nodes.firstOrNull;
|
||||
final delta = firstNode?.delta;
|
||||
final firstMarkdownNode = markdownNodes.firstOrNull;
|
||||
final firstMarkdownDelta = firstMarkdownNode?.delta;
|
||||
if (firstNode != null &&
|
||||
delta != null &&
|
||||
firstMarkdownNode != null &&
|
||||
firstMarkdownDelta != null) {
|
||||
final startIndex = selection.startIndex;
|
||||
final length = delta.length - startIndex;
|
||||
|
||||
transaction
|
||||
..deleteText(firstNode, startIndex, length)
|
||||
..insertTextDelta(firstNode, startIndex, firstMarkdownDelta);
|
||||
}
|
||||
|
||||
// step 2
|
||||
final lastNode = nodes.lastOrNull;
|
||||
final lastDelta = lastNode?.delta;
|
||||
final lastMarkdownNode = markdownNodes.lastOrNull;
|
||||
final lastMarkdownDelta = lastMarkdownNode?.delta;
|
||||
if (lastNode != null &&
|
||||
lastDelta != null &&
|
||||
lastMarkdownNode != null &&
|
||||
lastMarkdownDelta != null) {
|
||||
final endIndex = selection.endIndex;
|
||||
|
||||
transaction.deleteText(lastNode, 0, endIndex);
|
||||
|
||||
// if the last node is same as the first node, it means we have replaced the
|
||||
// selected text in the first node.
|
||||
if (lastMarkdownNode.id != firstMarkdownNode?.id) {
|
||||
transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta);
|
||||
}
|
||||
}
|
||||
|
||||
// step 3
|
||||
final insertedPath = selection.start.path.nextNPath(1);
|
||||
if (markdownNodes.length > 2) {
|
||||
transaction.insertNodes(
|
||||
insertedPath,
|
||||
markdownNodes.skip(1).take(markdownNodes.length - 2).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// step 4
|
||||
final length = nodes.length - 2;
|
||||
if (length > 0) {
|
||||
final middleNodes = nodes.skip(1).take(length).toList();
|
||||
transaction.deleteNodes(middleNodes);
|
||||
}
|
||||
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
class AINodeExternalValues extends NodeExternalValues {
|
||||
|
|
|
@ -47,7 +47,6 @@ class _CopyButton extends StatelessWidget {
|
|||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -43,13 +43,11 @@ extension PasteFromBlockLink on EditorState {
|
|||
node,
|
||||
selection.startIndex,
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.page.name,
|
||||
MentionBlockKeys.blockId: blockId,
|
||||
MentionBlockKeys.pageId: pageId,
|
||||
},
|
||||
},
|
||||
attributes: MentionBlockKeys.buildMentionPageAttributes(
|
||||
mentionType: MentionType.page,
|
||||
pageId: pageId,
|
||||
blockId: blockId,
|
||||
),
|
||||
);
|
||||
await apply(transaction);
|
||||
|
||||
|
|
|
@ -73,7 +73,6 @@ extension PasteFromImage on EditorState {
|
|||
Log.info('unsupported format: $format');
|
||||
if (UniversalPlatform.isMobile) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(),
|
||||
);
|
||||
}
|
||||
|
@ -112,7 +111,6 @@ extension PasteFromImage on EditorState {
|
|||
|
||||
if (errorMessage != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: errorMessage,
|
||||
);
|
||||
return false;
|
||||
|
@ -131,7 +129,6 @@ extension PasteFromImage on EditorState {
|
|||
Log.error('cannot copy image file', e);
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_imageBlock_error_invalidImage.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -241,7 +241,6 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
|||
.setData(ClipboardServiceData(plainText: href));
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -154,7 +154,6 @@ class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
|||
|
||||
void _copyBlockContent() {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),
|
||||
);
|
||||
|
||||
|
|
|
@ -105,7 +105,6 @@ Future<void> downloadMediaFile(
|
|||
} else {
|
||||
if (userProfile == null) {
|
||||
return showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_media_downloadFailedToken.tr(),
|
||||
);
|
||||
}
|
||||
|
@ -128,14 +127,12 @@ Future<void> downloadMediaFile(
|
|||
|
||||
if (result != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.grid_media_downloadSuccess.tr(),
|
||||
);
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
||||
);
|
||||
|
@ -159,13 +156,11 @@ Future<void> downloadMediaFile(
|
|||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_media_downloadSuccess.tr(),
|
||||
);
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
|||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
@ -113,24 +114,21 @@ class _RawEmojiIconWidgetState extends State<RawEmojiIconWidget> {
|
|||
try {
|
||||
switch (widget.emoji.type) {
|
||||
case FlowyIconType.emoji:
|
||||
return EmojiText(
|
||||
emoji: widget.emoji.emoji,
|
||||
return FlowyText.emoji(
|
||||
widget.emoji.emoji,
|
||||
fontSize: widget.emojiSize,
|
||||
textAlign: TextAlign.justify,
|
||||
lineHeight: widget.lineHeight,
|
||||
);
|
||||
case FlowyIconType.icon:
|
||||
IconsData iconData =
|
||||
IconsData.fromJson(jsonDecode(widget.emoji.emoji));
|
||||
IconsData iconData = IconsData.fromJson(
|
||||
jsonDecode(widget.emoji.emoji),
|
||||
);
|
||||
if (!widget.enableColor) {
|
||||
iconData = iconData.noColor();
|
||||
}
|
||||
|
||||
/// Under the same width conditions, icons on macOS seem to appear
|
||||
/// larger than emojis, so 0.9 is used here to slightly reduce the
|
||||
/// size of the icons
|
||||
final iconSize =
|
||||
Platform.isMacOS ? widget.emojiSize * 0.9 : widget.emojiSize;
|
||||
final iconSize = widget.emojiSize;
|
||||
return IconWidget(
|
||||
iconsData: iconData,
|
||||
size: iconSize,
|
||||
|
|
|
@ -378,7 +378,6 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
|
|||
onTap: () async {
|
||||
context.pop();
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
||||
);
|
||||
await getIt<ClipboardService>().setPlainText(url);
|
||||
|
@ -431,7 +430,6 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
|
|||
);
|
||||
if (mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: result.isSuccess
|
||||
? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr()
|
||||
: LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(),
|
||||
|
|
|
@ -117,14 +117,12 @@ class _ImageMenuState extends State<ImageMenu> {
|
|||
|
||||
if (mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_fail.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
|
|
@ -218,7 +218,6 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
|
|||
ClipboardData(text: images[widget.indexNotifier.value].url),
|
||||
);
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -78,7 +78,6 @@ class _LinkPreviewMenuState extends State<LinkPreviewMenu> {
|
|||
if (url != null) {
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_urlPreview_copiedToPasteBoard.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -100,7 +100,6 @@ class ChildPageTransactionHandler extends MentionTransactionHandler {
|
|||
Log.error(error);
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage
|
||||
.tr(),
|
||||
);
|
||||
|
@ -179,13 +178,6 @@ class ChildPageTransactionHandler extends MentionTransactionHandler {
|
|||
|
||||
await duplicatedViewOrFailure.fold(
|
||||
(newView) async {
|
||||
final newMentionAttributes = {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.childPage.name,
|
||||
MentionBlockKeys.pageId: newView.id,
|
||||
},
|
||||
};
|
||||
|
||||
// The index is the index of the delta, to get the index of the mention character
|
||||
// in all the text, we need to calculate it based on the deltas before the current delta.
|
||||
int mentionIndex = 0;
|
||||
|
@ -202,7 +194,11 @@ class ChildPageTransactionHandler extends MentionTransactionHandler {
|
|||
node,
|
||||
mentionIndex,
|
||||
MentionBlockKeys.mentionChar.length,
|
||||
newMentionAttributes,
|
||||
MentionBlockKeys.buildMentionPageAttributes(
|
||||
mentionType: MentionType.childPage,
|
||||
pageId: newView.id,
|
||||
blockId: null,
|
||||
),
|
||||
);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
|
|
|
@ -192,15 +192,12 @@ class DateTransactionHandler extends MentionTransactionHandler {
|
|||
),
|
||||
);
|
||||
|
||||
final newMentionAttributes = {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: dateTime.toIso8601String(),
|
||||
MentionBlockKeys.reminderId: reminderId,
|
||||
MentionBlockKeys.includeTime: data.includeTime,
|
||||
MentionBlockKeys.reminderOption: data.reminderOption.name,
|
||||
},
|
||||
};
|
||||
final newMentionAttributes = MentionBlockKeys.buildMentionDateAttributes(
|
||||
date: dateTime.toIso8601String(),
|
||||
reminderId: reminderId,
|
||||
reminderOption: data.reminderOption.name,
|
||||
includeTime: data.includeTime,
|
||||
);
|
||||
|
||||
// The index is the index of the delta, to get the index of the mention character
|
||||
// in all the text, we need to calculate it based on the deltas before the current delta.
|
||||
|
|
|
@ -27,12 +27,12 @@ Node dateMentionNode() {
|
|||
operations: [
|
||||
TextInsert(
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: DateTime.now().toIso8601String(),
|
||||
},
|
||||
},
|
||||
attributes: MentionBlockKeys.buildMentionDateAttributes(
|
||||
date: DateTime.now().toIso8601String(),
|
||||
reminderId: null,
|
||||
reminderOption: null,
|
||||
includeTime: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -42,18 +42,51 @@ Node dateMentionNode() {
|
|||
class MentionBlockKeys {
|
||||
const MentionBlockKeys._();
|
||||
|
||||
static const reminderId = 'reminder_id'; // ReminderID
|
||||
static const mention = 'mention';
|
||||
static const type = 'type'; // MentionType, String
|
||||
|
||||
static const pageId = 'page_id';
|
||||
static const blockId = 'block_id';
|
||||
|
||||
// Related to Reminder and Date blocks
|
||||
static const date = 'date'; // Start Date
|
||||
static const includeTime = 'include_time';
|
||||
static const reminderId = 'reminder_id'; // ReminderID
|
||||
static const reminderOption = 'reminder_option';
|
||||
|
||||
static const mentionChar = '\$';
|
||||
|
||||
static Map<String, dynamic> buildMentionPageAttributes({
|
||||
required MentionType mentionType,
|
||||
required String pageId,
|
||||
required String? blockId,
|
||||
}) {
|
||||
return {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: mentionType.name,
|
||||
MentionBlockKeys.pageId: pageId,
|
||||
if (blockId != null) MentionBlockKeys.blockId: blockId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> buildMentionDateAttributes({
|
||||
required String date,
|
||||
required String? reminderId,
|
||||
required String? reminderOption,
|
||||
required bool includeTime,
|
||||
}) {
|
||||
return {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: date,
|
||||
MentionBlockKeys.includeTime: includeTime,
|
||||
if (reminderId != null) MentionBlockKeys.reminderId: reminderId,
|
||||
if (reminderOption != null)
|
||||
MentionBlockKeys.reminderOption: reminderOption,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MentionBlock extends StatelessWidget {
|
||||
|
|
|
@ -201,16 +201,17 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
|||
(reminderOption == ReminderOption.none ? null : widget.reminderId);
|
||||
|
||||
final transaction = widget.editorState.transaction
|
||||
..formatText(widget.node, widget.index, 1, {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: date.toIso8601String(),
|
||||
MentionBlockKeys.reminderId: rId,
|
||||
MentionBlockKeys.includeTime: includeTime,
|
||||
MentionBlockKeys.reminderOption:
|
||||
reminderOption?.name ?? widget.reminderOption.name,
|
||||
},
|
||||
});
|
||||
..formatText(
|
||||
widget.node,
|
||||
widget.index,
|
||||
1,
|
||||
MentionBlockKeys.buildMentionDateAttributes(
|
||||
date: date.toIso8601String(),
|
||||
reminderId: rId,
|
||||
includeTime: includeTime,
|
||||
reminderOption: reminderOption?.name ?? widget.reminderOption.name,
|
||||
),
|
||||
);
|
||||
|
||||
widget.editorState.apply(transaction, withUpdateSelection: false);
|
||||
|
||||
|
|
|
@ -50,12 +50,11 @@ Node pageMentionNode(String viewId) {
|
|||
operations: [
|
||||
TextInsert(
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.page.name,
|
||||
MentionBlockKeys.pageId: viewId,
|
||||
},
|
||||
},
|
||||
attributes: MentionBlockKeys.buildMentionPageAttributes(
|
||||
mentionType: MentionType.page,
|
||||
pageId: viewId,
|
||||
blockId: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -284,12 +283,11 @@ class _MentionSubPageBlockState extends State<MentionSubPageBlock> {
|
|||
widget.node,
|
||||
widget.index,
|
||||
MentionBlockKeys.mentionChar.length,
|
||||
{
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.page.name,
|
||||
MentionBlockKeys.pageId: widget.pageId,
|
||||
},
|
||||
},
|
||||
MentionBlockKeys.buildMentionPageAttributes(
|
||||
mentionType: MentionType.page,
|
||||
pageId: widget.pageId,
|
||||
blockId: null,
|
||||
),
|
||||
);
|
||||
|
||||
widget.editorState.apply(
|
||||
|
@ -383,25 +381,24 @@ Future<void> _handleDoubleTap(
|
|||
}
|
||||
|
||||
final currentViewId = context.read<DocumentBloc>().documentId;
|
||||
final newViewId = await showPageSelectorSheet(
|
||||
final newView = await showPageSelectorSheet(
|
||||
context,
|
||||
currentViewId: currentViewId,
|
||||
selectedViewId: viewId,
|
||||
);
|
||||
|
||||
if (newViewId != null) {
|
||||
if (newView != null) {
|
||||
// Update this nodes pageId
|
||||
final transaction = editorState.transaction
|
||||
..formatText(
|
||||
node,
|
||||
index,
|
||||
1,
|
||||
{
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.page.name,
|
||||
MentionBlockKeys.pageId: newViewId,
|
||||
},
|
||||
},
|
||||
MentionBlockKeys.buildMentionPageAttributes(
|
||||
mentionType: MentionType.page,
|
||||
pageId: newView.id,
|
||||
blockId: null,
|
||||
),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction, withUpdateSelection: false);
|
||||
|
|
|
@ -46,12 +46,12 @@ extension on EditorState {
|
|||
selection.start.offset,
|
||||
0,
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: DateTime.now().toIso8601String(),
|
||||
},
|
||||
},
|
||||
attributes: MentionBlockKeys.buildMentionDateAttributes(
|
||||
date: DateTime.now().toIso8601String(),
|
||||
reminderId: null,
|
||||
reminderOption: null,
|
||||
includeTime: false,
|
||||
),
|
||||
);
|
||||
|
||||
await apply(transaction);
|
||||
|
|
|
@ -568,7 +568,6 @@ class EditorStyleCustomizer {
|
|||
if (style == null) {
|
||||
return null;
|
||||
}
|
||||
final fontSize = style.fontSize ?? 14.0;
|
||||
final isLight = Theme.of(context).isLightMode;
|
||||
final textColor = isLight ? Color(0xFF007296) : Color(0xFF49CFF4);
|
||||
final underlineColor = isLight ? Color(0x33005A7A) : Color(0x3349CFF4);
|
||||
|
@ -578,17 +577,10 @@ class EditorStyleCustomizer {
|
|||
decoration: TextDecoration.lineThrough,
|
||||
),
|
||||
AiWriterBlockKeys.suggestionReplacement => style.copyWith(
|
||||
color: Colors.transparent,
|
||||
color: textColor,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: underlineColor,
|
||||
decorationThickness: 1.0,
|
||||
// hack: https://jtmuller5.medium.com/the-ultimate-guide-to-underlining-text-in-flutter-57936f5c79bb
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: textColor,
|
||||
offset: Offset(0, -fontSize * 0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
_ => style,
|
||||
};
|
||||
|
|
|
@ -38,10 +38,6 @@ Future<bool> emojiCommandHandler(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!selection.isCollapsed) {
|
||||
await editorState.deleteSelection(selection);
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null ||
|
||||
|
@ -58,6 +54,8 @@ Future<bool> emojiCommandHandler(
|
|||
if (previousCharacter != _emojiCharacter) return false;
|
||||
if (!context.mounted) return false;
|
||||
|
||||
if (!selection.isCollapsed) return false;
|
||||
|
||||
await editorState.insertTextAtPosition(
|
||||
character,
|
||||
position: selection.start,
|
||||
|
|
|
@ -24,7 +24,7 @@ class InlineChildPageService extends InlineActionsDelegate {
|
|||
results.add(
|
||||
InlineActionsMenuItem(
|
||||
label: LocaleKeys.inlineActions_createPage.tr(args: [search]),
|
||||
icon: (_) => const FlowySvg(FlowySvgs.add_s),
|
||||
iconBuilder: (_) => const FlowySvg(FlowySvgs.add_s),
|
||||
onSelected: (context, editorState, service, replacement) =>
|
||||
_onSelected(context, editorState, service, replacement, search),
|
||||
),
|
||||
|
@ -71,12 +71,11 @@ class InlineChildPageService extends InlineActionsDelegate {
|
|||
replacement.$1,
|
||||
replacement.$2,
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.childPage.name,
|
||||
MentionBlockKeys.pageId: view.id,
|
||||
},
|
||||
},
|
||||
attributes: MentionBlockKeys.buildMentionPageAttributes(
|
||||
mentionType: MentionType.childPage,
|
||||
pageId: view.id,
|
||||
blockId: null,
|
||||
),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
|
|
|
@ -122,12 +122,12 @@ class DateReferenceService extends InlineActionsDelegate {
|
|||
start,
|
||||
end,
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: date.toIso8601String(),
|
||||
},
|
||||
},
|
||||
attributes: MentionBlockKeys.buildMentionDateAttributes(
|
||||
date: date.toIso8601String(),
|
||||
includeTime: false,
|
||||
reminderId: null,
|
||||
reminderOption: null,
|
||||
),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
|
|
|
@ -221,12 +221,11 @@ class InlinePageReferenceService extends InlineActionsDelegate {
|
|||
replace.$1,
|
||||
replace.$2,
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.page.name,
|
||||
MentionBlockKeys.pageId: view.id,
|
||||
},
|
||||
},
|
||||
attributes: MentionBlockKeys.buildMentionPageAttributes(
|
||||
mentionType: MentionType.page,
|
||||
pageId: view.id,
|
||||
blockId: null,
|
||||
),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
|
@ -235,12 +234,19 @@ class InlinePageReferenceService extends InlineActionsDelegate {
|
|||
InlineActionsMenuItem _fromView(ViewPB view) => InlineActionsMenuItem(
|
||||
keywords: [view.nameOrDefault.toLowerCase()],
|
||||
label: view.nameOrDefault,
|
||||
icon: (onSelected) => view.icon.value.isNotEmpty
|
||||
? RawEmojiIconWidget(
|
||||
emoji: view.icon.toEmojiIconData(),
|
||||
emojiSize: 14,
|
||||
)
|
||||
: view.defaultIcon(),
|
||||
iconBuilder: (onSelected) {
|
||||
final child = view.icon.value.isNotEmpty
|
||||
? RawEmojiIconWidget(
|
||||
emoji: view.icon.toEmojiIconData(),
|
||||
emojiSize: 16.0,
|
||||
lineHeight: 18.0 / 16.0,
|
||||
)
|
||||
: view.defaultIcon(size: const Size(16, 16));
|
||||
return SizedBox(
|
||||
width: 16,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
onSelected: (context, editorState, menu, replace) => insertPage
|
||||
? _onInsertPageRef(view, context, editorState, replace)
|
||||
: _onInsertLinkRef(view, context, editorState, menu, replace),
|
||||
|
|
|
@ -148,14 +148,12 @@ class ReminderReferenceService extends InlineActionsDelegate {
|
|||
start,
|
||||
end,
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: date.toIso8601String(),
|
||||
MentionBlockKeys.reminderId: reminder.id,
|
||||
MentionBlockKeys.reminderOption: ReminderOption.atTimeOfEvent.name,
|
||||
},
|
||||
},
|
||||
attributes: MentionBlockKeys.buildMentionDateAttributes(
|
||||
date: date.toIso8601String(),
|
||||
reminderId: reminder.id,
|
||||
reminderOption: ReminderOption.atTimeOfEvent.name,
|
||||
includeTime: false,
|
||||
),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
|
|
|
@ -12,13 +12,13 @@ typedef SelectItemHandler = void Function(
|
|||
class InlineActionsMenuItem {
|
||||
InlineActionsMenuItem({
|
||||
required this.label,
|
||||
this.icon,
|
||||
this.iconBuilder,
|
||||
this.keywords,
|
||||
this.onSelected,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final Widget Function(bool onSelected)? icon;
|
||||
final Widget Function(bool onSelected)? iconBuilder;
|
||||
final List<String>? keywords;
|
||||
final SelectItemHandler? onSelected;
|
||||
}
|
||||
|
|
|
@ -92,8 +92,8 @@ class InlineActionsWidget extends StatefulWidget {
|
|||
class _InlineActionsWidgetState extends State<InlineActionsWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = widget.item.icon;
|
||||
final hasIcon = icon != null;
|
||||
final iconBuilder = widget.item.iconBuilder;
|
||||
final hasIcon = iconBuilder != null;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: SizedBox(
|
||||
|
@ -104,7 +104,7 @@ class _InlineActionsWidgetState extends State<InlineActionsWidget> {
|
|||
text: Row(
|
||||
children: [
|
||||
if (hasIcon) ...[
|
||||
icon.call(widget.isSelected),
|
||||
iconBuilder.call(widget.isSelected),
|
||||
SizedBox(width: 12),
|
||||
],
|
||||
Flexible(
|
||||
|
|
|
@ -174,11 +174,10 @@ class ExportTab extends StatelessWidget {
|
|||
ClipboardServiceData(plainText: markdown),
|
||||
);
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copiedNotification.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) {
|
||||
state.publishResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_publishSuccessfully.tr(),
|
||||
),
|
||||
(error) => showToastNotification(
|
||||
context,
|
||||
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
|
||||
type: ToastificationType.error,
|
||||
),
|
||||
|
@ -97,11 +95,9 @@ class PublishTab extends StatelessWidget {
|
|||
} else if (state.unpublishResult != null) {
|
||||
state.unpublishResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||
),
|
||||
(error) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishFailed.tr(),
|
||||
description: error.msg,
|
||||
type: ToastificationType.error,
|
||||
|
@ -110,14 +106,12 @@ class PublishTab extends StatelessWidget {
|
|||
} else if (state.updatePathNameResult != null) {
|
||||
state.updatePathNameResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||
),
|
||||
(error) {
|
||||
Log.error('update path name failed: $error');
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),
|
||||
type: ToastificationType.error,
|
||||
description: error.code.publishErrorMessage,
|
||||
|
@ -182,8 +176,7 @@ class _PublishedWidgetState extends State<_PublishedWidget> {
|
|||
);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copy.tr(),
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
},
|
||||
onSubmitted: (pathName) {
|
||||
|
@ -292,7 +285,6 @@ class _PublishWidgetState extends State<_PublishWidget> {
|
|||
// check if any database is selected
|
||||
if (_selectedViews.isEmpty) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_noDatabaseSelected.tr(),
|
||||
);
|
||||
return;
|
||||
|
@ -611,7 +603,6 @@ class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> {
|
|||
// unable to deselect the primary database
|
||||
if (isPrimaryDatabase) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(),
|
||||
);
|
||||
|
|
|
@ -70,7 +70,6 @@ class ShareButton extends StatelessWidget {
|
|||
case ShareType.html:
|
||||
case ShareType.csv:
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
break;
|
||||
|
@ -81,7 +80,6 @@ class ShareButton extends StatelessWidget {
|
|||
|
||||
void _handleExportError(BuildContext context, FlowyError error) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
'${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}',
|
||||
);
|
||||
|
|
|
@ -117,8 +117,7 @@ class _ShareTabContent extends StatelessWidget {
|
|||
);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copy.tr(),
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class FlowyErrorPage extends StatelessWidget {
|
||||
factory FlowyErrorPage.error(
|
||||
|
@ -86,7 +90,9 @@ class FlowyErrorPage extends StatelessWidget {
|
|||
Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerDown: (_) async {
|
||||
await Clipboard.setData(ClipboardData(text: message));
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(plainText: message),
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
@ -188,8 +194,8 @@ class StackTracePreview extends StatelessWidget {
|
|||
"Copy",
|
||||
),
|
||||
useIntrinsicWidth: true,
|
||||
onTap: () => Clipboard.setData(
|
||||
ClipboardData(text: stackTrace),
|
||||
onTap: () => getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(plainText: stackTrace),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -252,18 +258,14 @@ class GitHubRedirectButton extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
leftIconSize: const Size.square(_height),
|
||||
text: const FlowyText(
|
||||
"AppFlowy",
|
||||
),
|
||||
text: FlowyText(LocaleKeys.appName.tr()),
|
||||
useIntrinsicWidth: true,
|
||||
leftIcon: const Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: FlowySvg(FlowySvgData('login/github-mark')),
|
||||
),
|
||||
onTap: () async {
|
||||
if (await canLaunchUrl(_gitHubNewBugUri)) {
|
||||
await launchUrl(_gitHubNewBugUri);
|
||||
}
|
||||
await afLaunchUri(_gitHubNewBugUri);
|
||||
},
|
||||
);
|
||||
}
|
|
@ -45,7 +45,6 @@ class _MobileSyncErrorPage extends StatelessWidget {
|
|||
onTapUp: () {
|
||||
getIt<ClipboardService>().setPlainText(error.toString());
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
bottomPadding: 0,
|
||||
);
|
||||
|
@ -101,7 +100,7 @@ class _DesktopSyncErrorPage extends StatelessWidget {
|
|||
onTapUp: () {
|
||||
getIt<ClipboardService>().setPlainText(error.toString());
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
bottomPadding: 0,
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ Future<String> customDocumentToMarkdown(
|
|||
Document document, {
|
||||
String path = '',
|
||||
AsyncValueSetter<Archive>? onArchive,
|
||||
String lineBreak = '',
|
||||
}) async {
|
||||
final List<Future<ArchiveFile>> fileFutures = [];
|
||||
|
||||
|
@ -41,6 +42,7 @@ Future<String> customDocumentToMarkdown(
|
|||
try {
|
||||
markdown = documentToMarkdown(
|
||||
document,
|
||||
lineBreak: lineBreak,
|
||||
customParsers: [
|
||||
const MathEquationNodeParser(),
|
||||
const CalloutNodeParser(),
|
||||
|
|
|
@ -99,6 +99,7 @@ class InitAppWidgetTask extends LaunchTask {
|
|||
Locale('zh', 'TW'),
|
||||
Locale('fa'),
|
||||
Locale('hin'),
|
||||
Locale('mr','IN'),
|
||||
],
|
||||
path: 'assets/translations',
|
||||
fallbackLocale: const Locale('en'),
|
||||
|
|
|
@ -136,7 +136,7 @@ class AppFlowyCloudDeepLink {
|
|||
final context = AppGlobals.rootNavKey.currentState?.context;
|
||||
if (context != null) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: err.msg,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,10 +73,10 @@ Future<Directory> appFlowyApplicationDataDirectory() async {
|
|||
case IntegrationMode.develop:
|
||||
final Directory documentsDir = await getApplicationSupportDirectory()
|
||||
.then((directory) => directory.create());
|
||||
return Directory(path.join(documentsDir.path, 'data_dev')).create();
|
||||
return Directory(path.join(documentsDir.path, 'data_dev'));
|
||||
case IntegrationMode.release:
|
||||
final Directory documentsDir = await getApplicationSupportDirectory();
|
||||
return Directory(path.join(documentsDir.path, 'data')).create();
|
||||
return Directory(path.join(documentsDir.path, 'data'));
|
||||
case IntegrationMode.unitTest:
|
||||
case IntegrationMode.integrationTest:
|
||||
return Directory(path.join(Directory.current.path, '.sandbox'));
|
||||
|
|
|
@ -8,7 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
|||
show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
|
||||
import '../../../generated/locale_keys.g.dart';
|
||||
import 'device_id.dart';
|
||||
|
@ -65,8 +64,7 @@ class BackendAuthService implements AuthService {
|
|||
Map<String, String> params = const {},
|
||||
}) async {
|
||||
const password = "Guest!@123456";
|
||||
final uid = uuid();
|
||||
final userEmail = "$uid@appflowy.io";
|
||||
final userEmail = "anon@appflowy.io";
|
||||
|
||||
final request = SignUpPayloadPB.create()
|
||||
..name = LocaleKeys.defaultUsername.tr()
|
||||
|
|
|
@ -17,14 +17,14 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) {
|
|||
case ErrorCode.InvalidEncryptSecret:
|
||||
case ErrorCode.NetworkError:
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: error.msg,
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: error.msg,
|
||||
type: ToastificationType.error,
|
||||
callbacks: ToastificationCallbacks(
|
||||
|
|
|
@ -76,7 +76,6 @@ class _ContinueWithEmailAndPasswordState
|
|||
) {
|
||||
if (!isEmail(email)) {
|
||||
return showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.signIn_invalidEmail.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/application/sign_in_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
class SignInWithMagicLinkButtons extends StatefulWidget {
|
||||
const SignInWithMagicLinkButtons({super.key});
|
||||
|
||||
@override
|
||||
State<SignInWithMagicLinkButtons> createState() =>
|
||||
_SignInWithMagicLinkButtonsState();
|
||||
}
|
||||
|
||||
class _SignInWithMagicLinkButtonsState
|
||||
extends State<SignInWithMagicLinkButtons> {
|
||||
final controller = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: UniversalPlatform.isMobile ? 38.0 : 48.0,
|
||||
child: FlowyTextField(
|
||||
autoFocus: false,
|
||||
focusNode: _focusNode,
|
||||
controller: controller,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14.0,
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onSubmitted: (_) => _sendMagicLink(context, controller.text),
|
||||
onTapOutside: (_) => _focusNode.unfocus(),
|
||||
),
|
||||
),
|
||||
const VSpace(12),
|
||||
_ConfirmButton(
|
||||
onTap: () => _sendMagicLink(context, controller.text),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMagicLink(BuildContext context, String email) {
|
||||
if (!isEmail(email)) {
|
||||
return showToastNotification(
|
||||
message: LocaleKeys.signIn_invalidEmail.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
}
|
||||
|
||||
context
|
||||
.read<SignInBloc>()
|
||||
.add(SignInEvent.signInWithMagicLink(email: email));
|
||||
|
||||
showConfirmDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.signIn_magicLinkSent.tr(),
|
||||
description: LocaleKeys.signIn_magicLinkSentDescription.tr(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConfirmButton extends StatelessWidget {
|
||||
const _ConfirmButton({
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SignInBloc, SignInState>(
|
||||
builder: (context, state) {
|
||||
final name = switch (state.loginType) {
|
||||
LoginType.signIn => LocaleKeys.signIn_signInWithMagicLink.tr(),
|
||||
LoginType.signUp => LocaleKeys.signIn_signUpWithMagicLink.tr(),
|
||||
};
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 32),
|
||||
maximumSize: const Size(double.infinity, 38),
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: FlowyText(
|
||||
name,
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: FlowyButton(
|
||||
isSelected: true,
|
||||
onTap: onTap,
|
||||
hoverColor: Theme.of(context).colorScheme.primary,
|
||||
text: FlowyText.medium(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
radius: Corners.s6Border,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ Future<void> shareLogFiles(BuildContext? context) async {
|
|||
if (archiveLogFiles.isEmpty) {
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: LocaleKeys.noLogFiles.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
@ -42,7 +42,7 @@ Future<void> shareLogFiles(BuildContext? context) async {
|
|||
if (zip == null) {
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: LocaleKeys.noLogFiles.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
@ -72,7 +72,7 @@ Future<void> shareLogFiles(BuildContext? context) async {
|
|||
} catch (e) {
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: e.toString(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
|
||||
const _appName = "AppFlowy";
|
||||
|
||||
/// Manages Local Notifications
|
||||
///
|
||||
/// Currently supports:
|
||||
|
@ -13,7 +12,7 @@ const _appName = "AppFlowy";
|
|||
///
|
||||
class NotificationService {
|
||||
static Future<void> initialize() async {
|
||||
await localNotifier.setup(appName: _appName);
|
||||
await localNotifier.setup(appName: LocaleKeys.appName.tr());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,13 @@ import 'dart:async';
|
|||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||
import 'package:appflowy/util/color_to_hex_string.dart';
|
||||
import 'package:appflowy/workspace/application/appearance_defaults.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
|
||||
|
@ -17,6 +19,7 @@ import 'package:flowy_infra/theme.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
part 'appearance_cubit.freezed.dart';
|
||||
|
||||
|
@ -97,7 +100,19 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
|
|||
Future<void> setTheme(String themeName) async {
|
||||
_appearanceSettings.theme = themeName;
|
||||
unawaited(_saveAppearanceSettings());
|
||||
emit(state.copyWith(appTheme: await AppTheme.fromName(themeName)));
|
||||
try {
|
||||
final theme = await AppTheme.fromName(themeName);
|
||||
emit(state.copyWith(appTheme: theme));
|
||||
} catch (e) {
|
||||
Log.error("Error setting theme: $e");
|
||||
if (UniversalPlatform.isMacOS) {
|
||||
showToastNotification(
|
||||
message:
|
||||
LocaleKeys.settings_workspacePage_theme_failedToLoadThemes.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the current user selected theme back to the default
|
||||
|
|
|
@ -169,7 +169,7 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> {
|
|||
|
||||
if (message != null) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: message,
|
||||
type: result.fold(
|
||||
(_) => ToastificationType.success,
|
||||
|
|
|
@ -632,7 +632,11 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||
Widget _buildViewIconButton() {
|
||||
final iconData = widget.view.icon.toEmojiIconData();
|
||||
final icon = iconData.isNotEmpty
|
||||
? RawEmojiIconWidget(emoji: iconData, emojiSize: 16.0)
|
||||
? RawEmojiIconWidget(
|
||||
emoji: iconData,
|
||||
emojiSize: 16.0,
|
||||
lineHeight: 18.0 / 16.0,
|
||||
)
|
||||
: Opacity(opacity: 0.6, child: widget.view.defaultIcon());
|
||||
|
||||
final Widget child = AppFlowyPopover(
|
||||
|
|
|
@ -193,7 +193,7 @@ Future<void> deleteMyAccount(
|
|||
|
||||
if (!isChecked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
type: ToastificationType.warning,
|
||||
bottomPadding: bottomPadding,
|
||||
message: LocaleKeys
|
||||
|
@ -208,7 +208,7 @@ Future<void> deleteMyAccount(
|
|||
|
||||
if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
type: ToastificationType.warning,
|
||||
bottomPadding: bottomPadding,
|
||||
message: LocaleKeys
|
||||
|
@ -226,7 +226,7 @@ Future<void> deleteMyAccount(
|
|||
|
||||
loading.stop();
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: LocaleKeys
|
||||
.newSettings_myAccount_deleteAccount_deleteAccountSuccess
|
||||
.tr(),
|
||||
|
@ -245,7 +245,7 @@ Future<void> deleteMyAccount(
|
|||
|
||||
loading.stop();
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
type: ToastificationType.error,
|
||||
bottomPadding: bottomPadding,
|
||||
message: f.msg,
|
||||
|
|
|
@ -157,7 +157,7 @@ class SettingsManageDataView extends StatelessWidget {
|
|||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
|
||||
message: LocaleKeys
|
||||
.settings_manageDataPage_cache_dialog_successHint
|
||||
.tr(),
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
|
|||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart';
|
||||
import 'package:appflowy/shared/error_page/error_page.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
|
@ -21,7 +22,6 @@ import 'package:flowy_infra/size.dart';
|
|||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
|
|
@ -53,8 +53,7 @@ class SettingsPageSitesEvent {
|
|||
);
|
||||
getIt<ClipboardService>().setData(ClipboardServiceData(plainText: url));
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copy.tr(),
|
||||
message: LocaleKeys.message_copy_success.tr(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -253,7 +253,6 @@ class _FreePlanUpgradeButton extends StatelessWidget {
|
|||
onTap: () {
|
||||
if (isOwner) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
LocaleKeys.settings_sites_namespace_redirectToPayment.tr(),
|
||||
type: ToastificationType.info,
|
||||
|
@ -264,7 +263,6 @@ class _FreePlanUpgradeButton extends StatelessWidget {
|
|||
);
|
||||
} else {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys
|
||||
.settings_sites_namespace_pleaseAskOwnerToSetHomePage
|
||||
.tr(),
|
||||
|
|
|
@ -216,7 +216,6 @@ class _DomainSettingsDialogState extends State<DomainSettingsDialog> {
|
|||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(),
|
||||
);
|
||||
|
||||
|
@ -234,7 +233,6 @@ class _DomainSettingsDialogState extends State<DomainSettingsDialog> {
|
|||
Log.error('Failed to update namespace: $f');
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: basicErrorMessage,
|
||||
type: ToastificationType.error,
|
||||
description: errorMessage,
|
||||
|
|
|
@ -203,7 +203,6 @@ class _PublishedViewSettingsDialogState
|
|||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
|
@ -212,7 +211,6 @@ class _PublishedViewSettingsDialogState
|
|||
Log.error('update path name failed: $f');
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),
|
||||
type: ToastificationType.error,
|
||||
description: f.code.publishErrorMessage,
|
||||
|
|
|
@ -178,7 +178,6 @@ class _SettingsSitesPageView extends StatelessWidget {
|
|||
Log.error('Failed to generate payment link for Pro Plan: ${f.msg}');
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
LocaleKeys.settings_sites_error_failedToGeneratePaymentLink.tr(),
|
||||
type: ToastificationType.error,
|
||||
|
@ -188,14 +187,12 @@ class _SettingsSitesPageView extends StatelessWidget {
|
|||
result != null) {
|
||||
result.fold((_) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||
);
|
||||
}, (f) {
|
||||
Log.error('Failed to unpublish view: ${f.msg}');
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishFailed.tr(),
|
||||
type: ToastificationType.error,
|
||||
description: f.msg,
|
||||
|
@ -204,14 +201,12 @@ class _SettingsSitesPageView extends StatelessWidget {
|
|||
} else if (type == SettingsSitesActionType.setHomePage && result != null) {
|
||||
result.fold((s) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_success_setHomepageSuccess.tr(),
|
||||
);
|
||||
}, (f) {
|
||||
Log.error('Failed to set homepage: ${f.msg}');
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_error_setHomepageFailed.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
@ -220,14 +215,12 @@ class _SettingsSitesPageView extends StatelessWidget {
|
|||
result != null) {
|
||||
result.fold((s) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_success_removeHomePageSuccess.tr(),
|
||||
);
|
||||
}, (f) {
|
||||
Log.error('Failed to remove homepage: ${f.msg}');
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_sites_error_removeHomePageFailed.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
|
|
@ -363,7 +363,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> {
|
|||
}) async {
|
||||
if (cloudUrl.isEmpty || webUrl.isEmpty) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
@ -375,7 +374,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> {
|
|||
if (mounted) {
|
||||
if (isValid) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_menu_changeUrl.tr(args: [cloudUrl]),
|
||||
);
|
||||
|
||||
|
@ -387,7 +385,6 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> {
|
|||
await runAppFlowy();
|
||||
} else {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
|
@ -522,7 +519,6 @@ class _SupportSettings extends StatelessWidget {
|
|||
await getIt<FlowyCacheManager>().clearAllCache();
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys
|
||||
.settings_manageDataPage_cache_dialog_successHint
|
||||
.tr(),
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:appflowy/env/cloud_env.dart';
|
|||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:appflowy/shared/error_page/error_page.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appflowy_cloud_setting_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart';
|
||||
|
@ -19,7 +20,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -71,7 +71,7 @@ class AppFlowyCloudViewSetting extends StatelessWidget {
|
|||
const VSpace(8),
|
||||
const AppFlowyCloudEnableSync(),
|
||||
const VSpace(6),
|
||||
const AppFlowyCloudSyncLogEnabled(),
|
||||
// const AppFlowyCloudSyncLogEnabled(),
|
||||
const VSpace(12),
|
||||
RestartButton(
|
||||
onClick: () {
|
||||
|
@ -130,7 +130,7 @@ class CustomAppFlowyCloudView extends StatelessWidget {
|
|||
final List<Widget> children = [];
|
||||
children.addAll([
|
||||
const AppFlowyCloudEnableSync(),
|
||||
const AppFlowyCloudSyncLogEnabled(),
|
||||
// const AppFlowyCloudSyncLogEnabled(),
|
||||
const VSpace(40),
|
||||
]);
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/error_page/error_page.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThemeUploadLearnMoreButton extends StatelessWidget {
|
||||
|
|
|
@ -157,7 +157,6 @@ class _NavigatorTextFieldDialogState extends State<NavigatorTextFieldDialog> {
|
|||
onOkPressed: () {
|
||||
if (newValue.isEmpty) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(),
|
||||
);
|
||||
return;
|
||||
|
@ -363,8 +362,7 @@ class OkCancelButton extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
void showToastNotification(
|
||||
BuildContext context, {
|
||||
void showToastNotification({
|
||||
String? message,
|
||||
TextSpan? richMessage,
|
||||
String? description,
|
||||
|
|
|
@ -51,7 +51,6 @@ class FlowyVersionSection extends CustomActionCell {
|
|||
}
|
||||
enableDocumentInternalLog = !enableDocumentInternalLog;
|
||||
showToastNotification(
|
||||
context,
|
||||
message: enableDocumentInternalLog
|
||||
? 'Enabled Internal Log'
|
||||
: 'Disabled Internal Log',
|
||||
|
|
|
@ -74,7 +74,6 @@ class ViewTitleBar extends StatelessWidget {
|
|||
listener: (context, state) {
|
||||
if (state.isLocked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,8 @@ String languageFromLocale(Locale locale) {
|
|||
default:
|
||||
return locale.languageCode;
|
||||
}
|
||||
case "mr":
|
||||
return "मराठी";
|
||||
case "he":
|
||||
return "עברית";
|
||||
case "hu":
|
||||
|
@ -79,7 +81,7 @@ String languageFromLocale(Locale locale) {
|
|||
case "ur":
|
||||
return "اردو";
|
||||
case "hin":
|
||||
return "हिन्दी";
|
||||
return "हिन्दी";
|
||||
}
|
||||
// If not found then the language code will be displayed
|
||||
return locale.languageCode;
|
||||
|
|
|
@ -98,8 +98,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "552f95f"
|
||||
resolved-ref: "552f95fd15627e10a138c6db2a6d0a8089bc9a25"
|
||||
ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
|
||||
resolved-ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "5.1.0"
|
||||
|
|
|
@ -2,13 +2,13 @@ name: appflowy
|
|||
description: Bring projects, wikis, and teams together with AI. AppFlowy is an
|
||||
AI collaborative workspace where you achieve more without losing control of
|
||||
your data. The best open source alternative to Notion.
|
||||
publish_to: "none"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.8.8
|
||||
version: 0.8.9
|
||||
|
||||
environment:
|
||||
flutter: ">=3.27.4"
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
flutter: '>=3.27.4'
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
any_date: ^1.0.4
|
||||
|
@ -39,7 +39,7 @@ dependencies:
|
|||
calendar_view:
|
||||
git:
|
||||
url: https://github.com/Xazin/flutter_calendar_view
|
||||
ref: "6fe0c98"
|
||||
ref: '6fe0c98'
|
||||
collection: ^1.17.1
|
||||
connectivity_plus: ^5.0.2
|
||||
cross_file: ^0.3.4+1
|
||||
|
@ -75,7 +75,7 @@ dependencies:
|
|||
flutter_emoji_mart:
|
||||
git:
|
||||
url: https://github.com/LucasXu0/emoji_mart.git
|
||||
ref: "355aa56"
|
||||
ref: '355aa56'
|
||||
flutter_math_fork: ^0.7.3
|
||||
flutter_slidable: ^3.0.0
|
||||
|
||||
|
@ -185,13 +185,13 @@ dependency_overrides:
|
|||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "552f95f"
|
||||
ref: '361b99c38370abeeb19656f89e8c31cb3666623b'
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git
|
||||
path: "packages/appflowy_editor_plugins"
|
||||
ref: "4efcff7"
|
||||
path: 'packages/appflowy_editor_plugins'
|
||||
ref: '4efcff7'
|
||||
|
||||
sheet:
|
||||
git:
|
||||
|
|
|
@ -375,7 +375,7 @@ void main() {
|
|||
await blocResponseFuture();
|
||||
bloc.runResponseAction(SuggestionAction.accept);
|
||||
await blocResponseFuture();
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
expect(
|
||||
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
|
||||
'Hello World',
|
||||
|
|
|
@ -294,6 +294,514 @@ void main() {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('markdown text robot - replace in same line:', () {
|
||||
final text1 =
|
||||
'''The introduction of the World Wide Web in the early 1990s marked a turning point. ''';
|
||||
final text2 =
|
||||
'''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. ''';
|
||||
final text3 =
|
||||
'''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';
|
||||
|
||||
Document buildTestDocument() {
|
||||
return Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
paragraphNode(delta: Delta()..insert(text1 + text2 + text3)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 1. create a document with a paragraph node
|
||||
// 2. use the text robot to replace the selected content in the same line
|
||||
// 3. check the document
|
||||
test('the selection is in the middle of the text', () async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: text1.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: text1.length + text2.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
'''Tim Berners-Lee's invention of the **World Wide Web** transformed the internet, making it accessible to _non-technical users_ and opening the floodgates for global mass adoption.''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterDelta = editorState.document.root.children[0].delta!.toList();
|
||||
expect(afterDelta.length, 5);
|
||||
|
||||
final d1 = afterDelta[0] as TextInsert;
|
||||
expect(d1.text, '${text1}Tim Berners-Lee\'s invention of the ');
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = afterDelta[1] as TextInsert;
|
||||
expect(d2.text, 'World Wide Web');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = afterDelta[2] as TextInsert;
|
||||
expect(d3.text, ' transformed the internet, making it accessible to ');
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = afterDelta[3] as TextInsert;
|
||||
expect(d4.text, 'non-technical users');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = afterDelta[4] as TextInsert;
|
||||
expect(
|
||||
d5.text,
|
||||
' and opening the floodgates for global mass adoption.$text3',
|
||||
);
|
||||
expect(d5.attributes, null);
|
||||
});
|
||||
|
||||
test('replace markdown text with selection from start to middle', () async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: text1.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
'''The **invention** of the _World Wide Web_ by Tim Berners-Lee transformed how we access information.''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterDelta = editorState.document.root.children[0].delta!.toList();
|
||||
expect(afterDelta.length, 5);
|
||||
|
||||
final d1 = afterDelta[0] as TextInsert;
|
||||
expect(d1.text, 'The ');
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = afterDelta[1] as TextInsert;
|
||||
expect(d2.text, 'invention');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = afterDelta[2] as TextInsert;
|
||||
expect(d3.text, ' of the ');
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = afterDelta[3] as TextInsert;
|
||||
expect(d4.text, 'World Wide Web');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = afterDelta[4] as TextInsert;
|
||||
expect(
|
||||
d5.text,
|
||||
' by Tim Berners-Lee transformed how we access information.$text2$text3',
|
||||
);
|
||||
expect(d5.attributes, null);
|
||||
});
|
||||
|
||||
test('replace markdown text with selection from middle to end', () async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: text1.length + text2.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: text1.length + text2.length + text3.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
'''**Email** became widespread, and instant messaging services like *ICQ* and **AOL Instant Messenger** gained tremendous popularity, allowing for seamless real-time text communication across the globe.''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterDelta = editorState.document.root.children[0].delta!.toList();
|
||||
expect(afterDelta.length, 7);
|
||||
|
||||
final d1 = afterDelta[0] as TextInsert;
|
||||
expect(
|
||||
d1.text,
|
||||
text1 + text2,
|
||||
);
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = afterDelta[1] as TextInsert;
|
||||
expect(d2.text, 'Email');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = afterDelta[2] as TextInsert;
|
||||
expect(
|
||||
d3.text,
|
||||
' became widespread, and instant messaging services like ',
|
||||
);
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = afterDelta[3] as TextInsert;
|
||||
expect(d4.text, 'ICQ');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = afterDelta[4] as TextInsert;
|
||||
expect(d5.text, ' and ');
|
||||
expect(d5.attributes, null);
|
||||
|
||||
final d6 = afterDelta[5] as TextInsert;
|
||||
expect(
|
||||
d6.text,
|
||||
'AOL Instant Messenger',
|
||||
);
|
||||
expect(d6.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d7 = afterDelta[6] as TextInsert;
|
||||
expect(
|
||||
d7.text,
|
||||
' gained tremendous popularity, allowing for seamless real-time text communication across the globe.',
|
||||
);
|
||||
expect(d7.attributes, null);
|
||||
});
|
||||
});
|
||||
|
||||
group('markdown text robot - replace in multiple lines:', () {
|
||||
final text1 =
|
||||
'''The introduction of the World Wide Web in the early 1990s marked a turning point. ''';
|
||||
final text2 =
|
||||
'''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. ''';
|
||||
final text3 =
|
||||
'''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';
|
||||
|
||||
Document buildTestDocument() {
|
||||
return Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
paragraphNode(delta: Delta()..insert(text1)),
|
||||
paragraphNode(delta: Delta()..insert(text2)),
|
||||
paragraphNode(delta: Delta()..insert(text3)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 1. create a document with 3 paragraph nodes
|
||||
// 2. use the text robot to replace the selected content in the multiple lines
|
||||
// 3. check the document
|
||||
test(
|
||||
'the selection starts with the first paragraph and ends with the middle of second paragraph',
|
||||
() async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
),
|
||||
end: Position(
|
||||
path: [1],
|
||||
offset: text2.length -
|
||||
', opening the floodgates for mass adoption. '.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
'''The **introduction** of the World Wide Web in the *early 1990s* marked a significant turning point.
|
||||
|
||||
Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterNodes = editorState.document.root.children;
|
||||
expect(afterNodes.length, 3);
|
||||
|
||||
{
|
||||
// first paragraph
|
||||
final delta1 = afterNodes[0].delta!.toList();
|
||||
expect(delta1.length, 5);
|
||||
|
||||
final d1 = delta1[0] as TextInsert;
|
||||
expect(d1.text, 'The ');
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta1[1] as TextInsert;
|
||||
expect(d2.text, 'introduction');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta1[2] as TextInsert;
|
||||
expect(d3.text, ' of the World Wide Web in the ');
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta1[3] as TextInsert;
|
||||
expect(d4.text, 'early 1990s');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta1[4] as TextInsert;
|
||||
expect(d5.text, ' marked a significant turning point.');
|
||||
expect(d5.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// second paragraph
|
||||
final delta2 = afterNodes[1].delta!.toList();
|
||||
expect(delta2.length, 3);
|
||||
|
||||
final d1 = delta2[0] as TextInsert;
|
||||
expect(d1.text, "Tim Berners-Lee's ");
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta2[1] as TextInsert;
|
||||
expect(d2.text, "revolutionary invention");
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta2[2] as TextInsert;
|
||||
expect(
|
||||
d3.text,
|
||||
" made the internet accessible to non-technical users, opening the floodgates for mass adoption. ",
|
||||
);
|
||||
expect(d3.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// third paragraph
|
||||
final delta3 = afterNodes[2].delta!.toList();
|
||||
expect(delta3.length, 1);
|
||||
|
||||
final d1 = delta3[0] as TextInsert;
|
||||
expect(d1.text, text3);
|
||||
expect(d1.attributes, null);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'the selection starts with the middle of the first paragraph and ends with the middle of last paragraph',
|
||||
() async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: 'The introduction of the World Wide Web'.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [2],
|
||||
offset:
|
||||
'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity'
|
||||
.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
''' in the **early 1990s** marked a *significant turning point* in technological history.
|
||||
|
||||
Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users, opening the floodgates for *unprecedented mass adoption*.
|
||||
|
||||
Email became **widely prevalent**, and instant messaging services like *ICQ* and *AOL Instant Messenger* gained tremendous popularity
|
||||
''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterNodes = editorState.document.root.children;
|
||||
expect(afterNodes.length, 3);
|
||||
|
||||
{
|
||||
// first paragraph
|
||||
final delta1 = afterNodes[0].delta!.toList();
|
||||
expect(delta1.length, 5);
|
||||
|
||||
final d1 = delta1[0] as TextInsert;
|
||||
expect(d1.text, 'The introduction of the World Wide Web in the ');
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta1[1] as TextInsert;
|
||||
expect(d2.text, 'early 1990s');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta1[2] as TextInsert;
|
||||
expect(d3.text, ' marked a ');
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta1[3] as TextInsert;
|
||||
expect(d4.text, 'significant turning point');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta1[4] as TextInsert;
|
||||
expect(d5.text, ' in technological history.');
|
||||
expect(d5.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// second paragraph
|
||||
final delta2 = afterNodes[1].delta!.toList();
|
||||
expect(delta2.length, 5);
|
||||
|
||||
final d1 = delta2[0] as TextInsert;
|
||||
expect(d1.text, "Tim Berners-Lee's ");
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta2[1] as TextInsert;
|
||||
expect(d2.text, "revolutionary invention");
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta2[2] as TextInsert;
|
||||
expect(
|
||||
d3.text,
|
||||
" made the internet accessible to non-technical users, opening the floodgates for ",
|
||||
);
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta2[3] as TextInsert;
|
||||
expect(d4.text, "unprecedented mass adoption");
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta2[4] as TextInsert;
|
||||
expect(d5.text, ".");
|
||||
expect(d5.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// third paragraph
|
||||
// third paragraph
|
||||
final delta3 = afterNodes[2].delta!.toList();
|
||||
expect(delta3.length, 7);
|
||||
|
||||
final d1 = delta3[0] as TextInsert;
|
||||
expect(d1.text, "Email became ");
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta3[1] as TextInsert;
|
||||
expect(d2.text, "widely prevalent");
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta3[2] as TextInsert;
|
||||
expect(d3.text, ", and instant messaging services like ");
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta3[3] as TextInsert;
|
||||
expect(d4.text, "ICQ");
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta3[4] as TextInsert;
|
||||
expect(d5.text, " and ");
|
||||
expect(d5.attributes, null);
|
||||
|
||||
final d6 = delta3[5] as TextInsert;
|
||||
expect(d6.text, "AOL Instant Messenger");
|
||||
expect(d6.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d7 = delta3[6] as TextInsert;
|
||||
expect(
|
||||
d7.text,
|
||||
" gained tremendous popularity, allowing for real-time text communication.",
|
||||
);
|
||||
expect(d7.attributes, null);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'the length of the returned response less than the length of the selected text',
|
||||
() async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: 'The introduction of the World Wide Web'.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [2],
|
||||
offset:
|
||||
'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity'
|
||||
.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
''' in the **early 1990s** marked a *significant turning point* in technological history.''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterNodes = editorState.document.root.children;
|
||||
expect(afterNodes.length, 2);
|
||||
|
||||
{
|
||||
// first paragraph
|
||||
final delta1 = afterNodes[0].delta!.toList();
|
||||
expect(delta1.length, 5);
|
||||
|
||||
final d1 = delta1[0] as TextInsert;
|
||||
expect(d1.text, "The introduction of the World Wide Web in the ");
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta1[1] as TextInsert;
|
||||
expect(d2.text, "early 1990s");
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta1[2] as TextInsert;
|
||||
expect(d3.text, " marked a ");
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta1[3] as TextInsert;
|
||||
expect(d4.text, "significant turning point");
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta1[4] as TextInsert;
|
||||
expect(d5.text, " in technological history.");
|
||||
expect(d5.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// second paragraph
|
||||
final delta2 = afterNodes[1].delta!.toList();
|
||||
expect(delta2.length, 1);
|
||||
|
||||
final d1 = delta2[0] as TextInsert;
|
||||
expect(d1.text, ", allowing for real-time text communication.");
|
||||
expect(d1.attributes, null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const _sample1 = '''# The Curious Cat
|
||||
|
|
|
@ -311,14 +311,16 @@
|
|||
"questionBubble": {
|
||||
"shortcuts": "الاختصارات",
|
||||
"whatsNew": "ما هو الجديد؟",
|
||||
"help": "المساعدة والدعم",
|
||||
"helpAndDocumentation": "المساعدة والتوثيق",
|
||||
"getSupport": "احصل على الدعم",
|
||||
"markdown": "Markdown",
|
||||
"debug": {
|
||||
"name": "معلومات التصحيح",
|
||||
"success": "تم نسخ معلومات التصحيح إلى الحافظة!",
|
||||
"fail": "تعذر نسخ معلومات التصحيح إلى الحافظة"
|
||||
},
|
||||
"feedback": "تعليق"
|
||||
"feedback": "تعليق",
|
||||
"help": "المساعدة والدعم"
|
||||
},
|
||||
"menuAppHeader": {
|
||||
"moreButtonToolTip": "إزالة وإعادة تسمية والمزيد...",
|
||||
|
@ -513,6 +515,8 @@
|
|||
"settings": "إعدادات",
|
||||
"members": "الأعضاء",
|
||||
"trash": "سلة المحذوفات",
|
||||
"helpAndDocumentation": "المساعدة والتوثيق",
|
||||
"getSupport": "احصل على الدعم",
|
||||
"helpAndSupport": "المساعدة والدعم"
|
||||
},
|
||||
"sites": {
|
||||
|
@ -1672,8 +1676,7 @@
|
|||
"url": {
|
||||
"launch": "فتح في المتصفح",
|
||||
"copy": "إنسخ الرابط",
|
||||
"textFieldHint": "أدخل عنوان URL",
|
||||
"copiedNotification": "تمت نسخها إلى الحافظة!"
|
||||
"textFieldHint": "أدخل عنوان URL"
|
||||
},
|
||||
"relation": {
|
||||
"relatedDatabasePlaceLabel": "قاعدة البيانات ذات الصلة",
|
||||
|
|
|
@ -133,14 +133,14 @@
|
|||
"questionBubble": {
|
||||
"shortcuts": "Dreceres",
|
||||
"whatsNew": "Què hi ha de nou?",
|
||||
"help": "Ajuda i Suport",
|
||||
"markdown": "Reducció",
|
||||
"debug": {
|
||||
"name": "Informació de depuració",
|
||||
"success": "S'ha copiat la informació de depuració!",
|
||||
"fail": "No es pot copiar la informació de depuració"
|
||||
},
|
||||
"feedback": "Feedback"
|
||||
"feedback": "Feedback",
|
||||
"help": "Ajuda i Suport"
|
||||
},
|
||||
"menuAppHeader": {
|
||||
"moreButtonToolTip": "Suprimeix, canvia el nom i més...",
|
||||
|
|
|
@ -170,14 +170,14 @@
|
|||
"questionBubble": {
|
||||
"shortcuts": "کورتە ڕێگاکان",
|
||||
"whatsNew": "نوێترین",
|
||||
"help": "پشتیوانی و یارمەتی",
|
||||
"markdown": "Markdown",
|
||||
"debug": {
|
||||
"name": "زانیاری دیباگ",
|
||||
"success": "زانیارییەکانی دیباگ کۆپی کراون بۆ کلیپبۆرد!",
|
||||
"fail": "ناتوانرێت زانیارییەکانی دیباگ کۆپی بکات بۆ کلیپبۆرد"
|
||||
},
|
||||
"feedback": "فیدباک"
|
||||
"feedback": "فیدباک",
|
||||
"help": "پشتیوانی و یارمەتی"
|
||||
},
|
||||
"menuAppHeader": {
|
||||
"moreButtonToolTip": "سڕینەوە، گۆڕینی ناو، و زۆر شتی تر...",
|
||||
|
|
|
@ -134,14 +134,14 @@
|
|||
"questionBubble": {
|
||||
"shortcuts": "Klávesové zkratky",
|
||||
"whatsNew": "Co je nového?",
|
||||
"help": "Pomoc a podpora",
|
||||
"markdown": "Markdown",
|
||||
"debug": {
|
||||
"name": "Debug informace",
|
||||
"success": "Debug informace zkopírovány do schránky!",
|
||||
"fail": "Nepodařilo se zkopáí"
|
||||
},
|
||||
"feedback": "Zpětná vazba"
|
||||
"feedback": "Zpětná vazba",
|
||||
"help": "Pomoc a podpora"
|
||||
},
|
||||
"menuAppHeader": {
|
||||
"moreButtonToolTip": "Smazat, přejmenovat, a další...",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue