Merge branch 'main' into sign_in_with_password_or_passcode

This commit is contained in:
LucasXu0 2025-04-10 10:29:06 +08:00
commit 0344afddf4
239 changed files with 9903 additions and 1882 deletions

View file

@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.8.8"
APPFLOWY_VERSION = "0.8.9"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"

File diff suppressed because it is too large Load diff

View file

@ -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);
});
});
}

View file

@ -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));
});
});
}

View file

@ -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);

View file

@ -133,7 +133,6 @@ Future<bool> _afLaunchLocalUri(
};
if (context != null && context.mounted) {
showToastNotification(
context,
message: message,
type: result.type == ResultType.done
? ToastificationType.success

View file

@ -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(),
);
}

View file

@ -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(),
);

View file

@ -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(),
);
},

View file

@ -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(),
);

View file

@ -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})');

View file

@ -329,7 +329,7 @@ class _HomePageState extends State<_HomePage> {
}
if (message != null) {
showToastNotification(context, message: message, type: toastType);
showToastNotification(message: message, type: toastType);
}
}
}

View file

@ -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(),
);

View file

@ -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(

View file

@ -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(),
);

View file

@ -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)',
);

View file

@ -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(),

View file

@ -74,7 +74,6 @@ class _NotificationTabState extends State<NotificationTab>
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_refreshSuccess.tr(),
);
}

View file

@ -81,7 +81,6 @@ class SupportSettingGroup extends StatelessWidget {
);
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
);
}

View file

@ -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(),
);

View file

@ -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(),
);
}
},

View file

@ -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(),
);
}
},

View file

@ -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(

View file

@ -241,6 +241,11 @@ class SelectOptionCellEditorBloc
} else if (!state.selectedOptions
.any((option) => option.id == focusedOptionId)) {
_selectOptionService.select(optionIds: [focusedOptionId]);
emit(
state.copyWith(
clearFilter: true,
),
);
}
}

View file

@ -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(

View file

@ -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) {

View file

@ -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();

View file

@ -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,
);

View file

@ -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(),
);
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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,
),
);
}

View file

@ -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 {

View file

@ -47,7 +47,6 @@ class _CopyButton extends StatelessWidget {
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
);
}

View file

@ -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);

View file

@ -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(),
);
}

View file

@ -241,7 +241,6 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
.setData(ClipboardServiceData(plainText: href));
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
);
}

View file

@ -154,7 +154,6 @@ class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
void _copyBlockContent() {
showToastNotification(
context,
message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),
);

View file

@ -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(),
);

View file

@ -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,

View file

@ -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(),

View file

@ -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,
);

View file

@ -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(),
);
}

View file

@ -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(),
);
}

View file

@ -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,

View file

@ -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.

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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,
};

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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),

View file

@ -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);

View file

@ -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;
}

View file

@ -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(

View file

@ -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),
);
}
}

View file

@ -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(),
);

View file

@ -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}',
);

View file

@ -117,8 +117,7 @@ class _ShareTabContent extends StatelessWidget {
);
showToastNotification(
context,
message: LocaleKeys.grid_url_copy.tr(),
message: LocaleKeys.message_copy_success.tr(),
);
}
}

View file

@ -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);
},
);
}

View file

@ -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,
);

View file

@ -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(),

View file

@ -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'),

View file

@ -136,7 +136,7 @@ class AppFlowyCloudDeepLink {
final context = AppGlobals.rootNavKey.currentState?.context;
if (context != null) {
showToastNotification(
context,
message: err.msg,
);
}

View file

@ -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'));

View file

@ -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()

View file

@ -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(

View file

@ -76,7 +76,6 @@ class _ContinueWithEmailAndPasswordState
) {
if (!isEmail(email)) {
return showToastNotification(
context,
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);

View file

@ -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,
),
);
}
},
);
}
}

View file

@ -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,
);

View file

@ -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());
}
}

View file

@ -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

View file

@ -169,7 +169,7 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> {
if (message != null) {
showToastNotification(
context,
message: message,
type: result.fold(
(_) => ToastificationType.success,

View file

@ -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(

View file

@ -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,

View file

@ -157,7 +157,7 @@ class SettingsManageDataView extends StatelessWidget {
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys
.settings_manageDataPage_cache_dialog_successHint
.tr(),

View file

@ -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';

View file

@ -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(),
);
}
}

View file

@ -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(),

View file

@ -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,

View file

@ -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,

View file

@ -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,
);

View file

@ -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(),

View file

@ -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),
]);

View file

@ -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 {

View file

@ -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,

View file

@ -51,7 +51,6 @@ class FlowyVersionSection extends CustomActionCell {
}
enableDocumentInternalLog = !enableDocumentInternalLog;
showToastNotification(
context,
message: enableDocumentInternalLog
? 'Enabled Internal Log'
: 'Disabled Internal Log',

View file

@ -74,7 +74,6 @@ class ViewTitleBar extends StatelessWidget {
listener: (context, state) {
if (state.isLocked) {
showToastNotification(
context,
message: LocaleKeys.lockPage_pageLockedToast.tr(),
);
}

View file

@ -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;

View file

@ -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"

View file

@ -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:

View file

@ -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',

View file

@ -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

View file

@ -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": "قاعدة البيانات ذات الصلة",

View file

@ -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...",

View file

@ -170,14 +170,14 @@
"questionBubble": {
"shortcuts": "کورتە ڕێگاکان",
"whatsNew": "نوێترین",
"help": "پشتیوانی و یارمەتی",
"markdown": "Markdown",
"debug": {
"name": "زانیاری دیباگ",
"success": "زانیارییەکانی دیباگ کۆپی کراون بۆ کلیپبۆرد!",
"fail": "ناتوانرێت زانیارییەکانی دیباگ کۆپی بکات بۆ کلیپبۆرد"
},
"feedback": "فیدباک"
"feedback": "فیدباک",
"help": "پشتیوانی و یارمەتی"
},
"menuAppHeader": {
"moreButtonToolTip": "سڕینەوە، گۆڕینی ناو، و زۆر شتی تر...",

View file

@ -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