diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart index 335f9a377f..ed30529149 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; +import 'package:appflowy/plugins/document/presentation/sync_error_page.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:flowy_infra/uuid.dart'; -import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -92,7 +92,7 @@ void main() { ); expect(finder, findsOneWidget); await tester.tapButton(finder); - expect(find.byType(FlowyErrorPage), findsOneWidget); + expect(find.byType(SyncErrorPage), findsOneWidget); }); }); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/gesture.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart similarity index 100% rename from frontend/appflowy_flutter/lib/mobile/presentation/base/gesture.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 28e3812b93..8742dce817 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; @@ -109,6 +109,7 @@ class _MobileWorkspace extends StatelessWidget { return const SizedBox.shrink(); } return AnimatedGestureDetector( + scaleFactor: 0.99, alignment: Alignment.centerLeft, onTapUp: () { context.read().add( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart index 85cdc98c4a..f5e031666a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart @@ -5,7 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart'; -import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart index 8ecd70f7e5..a5cb39ffaa 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart index 448f2033f4..ea57d5d391 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart @@ -1,5 +1,5 @@ import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart'; -import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart'; import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart index 0f17bba68c..8f6db18265 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart @@ -1,6 +1,6 @@ import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart'; -import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index d835a7c00b..ed4ed73dff 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; @@ -12,6 +11,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/cust import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/plugins/document/presentation/sync_error_page.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; @@ -22,8 +22,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:cross_file/cross_file.dart'; import 'package:desktop_drop/desktop_drop.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; @@ -115,9 +113,10 @@ class _DocumentPageState extends State final error = state.error; if (error != null || editorState == null) { Log.error(error); - return FlowyErrorPage.message( - error.toString(), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + return Center( + child: SyncErrorPage( + error: error, + ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 53dbf57c6d..f5be9246b3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -500,7 +500,7 @@ class _AppFlowyEditorPageState extends State { void _customizeBlockComponentBackgroundColorDecorator() { blockComponentBackgroundColorDecorator = (Node node, String colorString) { - if (context.mounted) { + if (mounted && context.mounted) { return buildEditorCustomizedColor(context, node, colorString); } return null; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart index d138e644cd..014889261f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -106,9 +107,9 @@ class _AppFlowyMobileToolbarIconItemState final enable = widget.enable?.call() ?? true; return Padding( padding: const EdgeInsets.symmetric(vertical: 5), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { + child: AnimatedGestureDetector( + scaleFactor: 0.95, + onTapUp: () { widget.onTap(); _rebuild(); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 28f82c07e6..19e0dcaa00 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -138,7 +138,7 @@ class EditorStyleCustomizer { fontSize: fontSize, fontWeight: FontWeight.normal, color: Colors.red, - backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8), + backgroundColor: Colors.grey.withOpacity(0.3), ), ), applyHeightToFirstAscent: true, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/sync_error_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/sync_error_page.dart new file mode 100644 index 0000000000..6318254284 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/sync_error_page.dart @@ -0,0 +1,168 @@ +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/mobile/presentation/base/animated_gesture.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' show PlatformExtension; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class SyncErrorPage extends StatelessWidget { + const SyncErrorPage({ + super.key, + this.error, + }); + + final FlowyError? error; + + @override + Widget build(BuildContext context) { + if (PlatformExtension.isMobile) { + return _MobileSyncErrorPage(error: error); + } else { + return _DesktopSyncErrorPage(error: error); + } + } +} + +class _MobileSyncErrorPage extends StatelessWidget { + const _MobileSyncErrorPage({ + this.error, + }); + + final FlowyError? error; + + @override + Widget build(BuildContext context) { + return AnimatedGestureDetector( + scaleFactor: 0.99, + onTapUp: () { + getIt().setPlainText(error.toString()); + showToastNotification( + context, + message: LocaleKeys.message_copy_success.tr(), + bottomPadding: 0, + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlowySvg( + FlowySvgs.icon_warning_xl, + blendMode: null, + ), + const VSpace(16.0), + FlowyText.medium( + LocaleKeys.error_syncError.tr(), + fontSize: 15, + ), + const VSpace(8.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: FlowyText.regular( + LocaleKeys.error_syncErrorHint.tr(), + fontSize: 13, + color: Theme.of(context).hintColor, + textAlign: TextAlign.center, + maxLines: 10, + ), + ), + const VSpace(2.0), + FlowyText.regular( + '(${LocaleKeys.error_clickToCopy.tr()})', + fontSize: 13, + color: Theme.of(context).hintColor, + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} + +class _DesktopSyncErrorPage extends StatelessWidget { + const _DesktopSyncErrorPage({ + this.error, + }); + + final FlowyError? error; + + @override + Widget build(BuildContext context) { + return AnimatedGestureDetector( + scaleFactor: 0.995, + onTapUp: () { + getIt().setPlainText(error.toString()); + showToastNotification( + context, + message: LocaleKeys.message_copy_success.tr(), + bottomPadding: 0, + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlowySvg( + FlowySvgs.icon_warning_xl, + blendMode: null, + ), + const VSpace(16.0), + FlowyText.medium( + error?.code.toString() ?? '', + fontSize: 16, + ), + const VSpace(8.0), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.errorDialog_howToFixFallbackHint1.tr(), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).hintColor, + ), + ), + TextSpan( + text: 'Github', + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + afLaunchUrlString( + 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?template=bug_report.yaml', + ); + }, + ), + TextSpan( + text: LocaleKeys.errorDialog_howToFixFallbackHint2.tr(), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).hintColor, + ), + ), + ], + ), + ), + const VSpace(8.0), + FlowyText.regular( + '(${LocaleKeys.error_clickToCopy.tr()})', + fontSize: 14, + color: Theme.of(context).hintColor, + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index 8f05e44185..ff95fe6acc 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -11,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -51,6 +52,14 @@ class ExportTab extends StatelessWidget { svg: FlowySvgs.duplicate_s, onTap: () => _exportToClipboard(context), ), + if (kDebugMode) ...[ + const VSpace(10), + _ExportButton( + title: 'JSON (Debug Mode)', + svg: FlowySvgs.duplicate_s, + onTap: () => _exportJSON(context), + ), + ], ], ); } @@ -64,6 +73,14 @@ class ExportTab extends StatelessWidget { svg: FlowySvgs.database_layout_m, onTap: () => _exportCSV(context), ), + if (kDebugMode) ...[ + const VSpace(10), + _ExportButton( + title: 'Raw Database Data (Debug Mode)', + svg: FlowySvgs.duplicate_s, + onTap: () => _exportRawDatabaseData(context), + ), + ], ], ); } @@ -100,6 +117,22 @@ class ExportTab extends StatelessWidget { } } + Future _exportJSON(BuildContext context) async { + final viewName = context.read().state.viewName; + final exportPath = await getIt().saveFile( + dialogTitle: '', + fileName: '${viewName.toFileName()}.json', + ); + if (context.mounted && exportPath != null) { + context.read().add( + ShareEvent.share( + ShareType.json, + exportPath, + ), + ); + } + } + Future _exportCSV(BuildContext context) async { final viewName = context.read().state.viewName; final exportPath = await getIt().saveFile( @@ -116,6 +149,22 @@ class ExportTab extends StatelessWidget { } } + Future _exportRawDatabaseData(BuildContext context) async { + final viewName = context.read().state.viewName; + final exportPath = await getIt().saveFile( + dialogTitle: '', + fileName: '${viewName.toFileName()}.json', + ); + if (context.mounted && exportPath != null) { + context.read().add( + ShareEvent.share( + ShareType.rawDatabaseData, + exportPath, + ), + ); + } + } + Future _exportToClipboard(BuildContext context) async { final documentExporter = DocumentExporter(context.read().view); final result = await documentExporter.export(DocumentExportType.markdown); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index 16d50d2212..af6adc4f68 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -179,6 +179,14 @@ class ShareBloc extends Bloc { (s) => FlowyResult.success(s.data), (f) => FlowyResult.failure(f), ); + } else if (type == ShareType.rawDatabaseData) { + final exportResult = await BackendExportService.exportDatabaseAsRawData( + view.id, + ); + result = exportResult.fold( + (s) => FlowyResult.success(s.data), + (f) => FlowyResult.failure(f), + ); } else { result = await documentExporter.export(type.documentExportType); } @@ -189,6 +197,8 @@ class ShareBloc extends Bloc { case ShareType.markdown: case ShareType.html: case ShareType.csv: + case ShareType.json: + case ShareType.rawDatabaseData: File(path).writeAsStringSync(s); return FlowyResult.success(type); default: @@ -208,9 +218,11 @@ enum ShareType { html, text, link, + json, // only available in database - csv; + csv, + rawDatabaseData; static List get unimplemented => [link]; @@ -222,10 +234,16 @@ enum ShareType { return DocumentExportType.html; case ShareType.text: return DocumentExportType.text; + case ShareType.json: + return DocumentExportType.json; case ShareType.csv: throw UnsupportedError('DocumentShareType.csv is not supported'); case ShareType.link: throw UnsupportedError('DocumentShareType.link is not supported'); + case ShareType.rawDatabaseData: + throw UnsupportedError( + 'DocumentShareType.rawDatabaseData is not supported', + ); } } } diff --git a/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart b/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart index 550c8609e5..e6ace027fa 100644 --- a/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart @@ -10,6 +10,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -1524,9 +1525,10 @@ class PopupMenuButtonState extends State> { assert(debugCheckHasMaterialLocalizations(context)); if (widget.child != null) { - return GestureDetector( - onTap: widget.enabled ? showButtonMenu : null, - child: widget.child, + return AnimatedGestureDetector( + scaleFactor: 0.99, + onTapUp: widget.enabled ? showButtonMenu : null, + child: widget.child!, ); } diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 85be02f6a6..60b18fd8d6 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -109,7 +109,8 @@ class FlowyRunner { [ // this task should be first task, for handling platform errors. // don't catch errors in test mode - if (!mode.isUnitTest) const PlatformErrorCatcherTask(), + if (!mode.isUnitTest && !mode.isIntegrationTest) + const PlatformErrorCatcherTask(), if (!mode.isUnitTest) const InitSentryTask(), // this task should be second task, for handling memory leak. // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored. diff --git a/frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart b/frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart index 9d088bb5d4..c2c64536b2 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart @@ -1,5 +1,7 @@ import 'package:appflowy_backend/log.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import '../startup.dart'; @@ -17,6 +19,23 @@ class PlatformErrorCatcherTask extends LaunchTask { return true; }; } + + ErrorWidget.builder = (details) { + if (kDebugMode) { + return Container( + width: double.infinity, + height: 30, + color: Colors.red, + child: FlowyText( + 'ERROR: ${details.exceptionAsString()}', + color: Colors.white, + ), + ); + } + + // hide the error widget in release mode + return const SizedBox.shrink(); + }; } @override diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 5526cb6c70..727062a108 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -44,6 +44,7 @@ class MobileSignInScreen extends StatelessWidget { const Spacer(flex: 2), const Spacer(), Expanded(child: _buildSettingsButton(context)), + if (Platform.isAndroid) const Spacer(), ], ), ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart index 47c6ea515f..c51634bcf5 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart index 7cf81b3bfb..e890959949 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart @@ -12,4 +12,12 @@ class BackendExportService { final payload = DatabaseViewIdPB.create()..value = viewId; return DatabaseEventExportCSV(payload).send(); } + + static Future> + exportDatabaseAsRawData( + String viewId, + ) async { + final payload = DatabaseViewIdPB.create()..value = viewId; + return DatabaseEventExportRawDatabaseData(payload).send(); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart index 41ef345755..03a8f48be2 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart @@ -452,6 +452,9 @@ class SpaceBloc extends Bloc { )..start( sectionChanged: (result) async { Log.info('did receive section views changed'); + if (isClosed) { + return; + } add(const SpaceEvent.didReceiveSpaceUpdate()); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart index c22bc95978..fa86082e74 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart @@ -1,17 +1,17 @@ -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; -import 'package:flutter/material.dart'; - +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/shared/feature_flags.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +import 'sidebar_footer_button.dart'; class SidebarFooter extends StatelessWidget { const SidebarFooter({super.key}); @@ -26,52 +26,56 @@ class SidebarFooter extends StatelessWidget { return const SidebarToast(); }, ), - const Row( - children: [ - Expanded(child: SidebarTrashButton()), - // Enable it when the widget button is ready - // SizedBox( - // height: 16, - // child: VerticalDivider(width: 1, color: Color(0x141F2329)), - // ), - // Expanded(child: SidebarWidgetButton()), - ], - ), + const SidebarTemplateButton(), + const SidebarTrashButton(), ], ); } } +class SidebarTemplateButton extends StatelessWidget { + const SidebarTemplateButton({super.key}); + + @override + Widget build(BuildContext context) { + return SidebarFooterButton( + leftIconSize: const Size.square(24.0), + leftIcon: const Padding( + padding: EdgeInsets.all(2.0), + child: FlowySvg( + FlowySvgs.icon_template_s, + ), + ), + text: LocaleKeys.template_label.tr(), + onTap: () => afLaunchUrlString('https://appflowy.io/templates'), + ); + } +} + class SidebarTrashButton extends StatelessWidget { const SidebarTrashButton({super.key}); @override Widget build(BuildContext context) { - return SizedBox( - height: HomeSizes.workspaceSectionHeight, - child: ValueListenableBuilder( - valueListenable: getIt().notifier, - builder: (context, value, child) { - return FlowyButton( - leftIcon: const FlowySvg(FlowySvgs.sidebar_footer_trash_m), - leftIconSize: const Size.square(24.0), - iconPadding: 8.0, - margin: const EdgeInsets.all(4.0), - text: FlowyText.regular( - LocaleKeys.trash_text.tr(), - lineHeight: 1.15, - ), - onTap: () { - getIt().latestOpenView = null; - getIt().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.trash), - ), - ); - }, - ); - }, - ), + return ValueListenableBuilder( + valueListenable: getIt().notifier, + builder: (context, value, child) { + return SidebarFooterButton( + leftIconSize: const Size.square(24.0), + leftIcon: const FlowySvg( + FlowySvgs.sidebar_footer_trash_m, + ), + text: LocaleKeys.trash_text.tr(), + onTap: () { + getIt().latestOpenView = null; + getIt().add( + TabsEvent.openPlugin( + plugin: makePlugin(pluginType: PluginType.trash), + ), + ); + }, + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer_button.dart new file mode 100644 index 0000000000..f83e1dd046 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer_button.dart @@ -0,0 +1,39 @@ +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +// This button style is used in +// - Trash button +// - Template button +class SidebarFooterButton extends StatelessWidget { + const SidebarFooterButton({ + super.key, + required this.leftIcon, + required this.leftIconSize, + required this.text, + required this.onTap, + }); + + final Widget leftIcon; + final Size leftIconSize; + final String text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: HomeSizes.workspaceSectionHeight, + child: FlowyButton( + leftIcon: leftIcon, + leftIconSize: leftIconSize, + iconPadding: 8.0, + margin: const EdgeInsets.all(4.0), + text: FlowyText.regular( + text, + lineHeight: 1.15, + ), + onTap: onTap, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 76a9421968..b8421235d3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -97,6 +97,13 @@ class _NavigatorTextFieldDialogState extends State { VSpace(Insets.xl), OkCancelButton( onOkPressed: () { + if (newValue.isEmpty) { + showToastNotification( + context, + message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(), + ); + return; + } widget.onConfirm(newValue, context); Navigator.of(context).pop(); }, diff --git a/frontend/resources/flowy_icons/16x/icon_template.svg b/frontend/resources/flowy_icons/16x/icon_template.svg new file mode 100644 index 0000000000..1b6af1bac6 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/icon_template.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/resources/flowy_icons/40x/icon_warning.svg b/frontend/resources/flowy_icons/40x/icon_warning.svg new file mode 100644 index 0000000000..abdf5fb20d --- /dev/null +++ b/frontend/resources/flowy_icons/40x/icon_warning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 2ff4a5a5f2..b2270cfd5d 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1897,6 +1897,8 @@ "errorDialog": { "title": "@:appName Error", "howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.", + "howToFixFallbackHint1": "We're sorry for the inconvenience! Submit an issue on our ", + "howToFixFallbackHint2": " page that describes your error.", "github": "View on GitHub" }, "search": { @@ -2051,7 +2053,10 @@ }, "error": { "weAreSorry": "We're sorry", - "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues." + "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues.", + "syncError": "Data is not synced from another device", + "syncErrorHint": "Please reopen this page on the device where it was last edited, then open it again on the current device.", + "clickToCopy": "Click to copy error code" }, "editor": { "bold": "Bold", @@ -2317,7 +2322,8 @@ "quicklySwitch": "Quickly switch to the next space", "duplicate": "Duplicate Space", "movePageToSpace": "Move page to space", - "switchSpace": "Switch space" + "switchSpace": "Switch space", + "spaceNameCannotBeEmpty": "Space name cannot be empty" }, "publish": { "hasNotBeenPublished": "This page hasn't been published yet", @@ -2484,7 +2490,8 @@ "addRelatedTemplate": "Add related template", "removeRelatedTemplate": "Remove related template", "uploadAvatar": "Upload avatar", - "searchInCategory": "Search in {category}" + "searchInCategory": "Search in {category}", + "label": "Template" }, "fileDropzone": { "dropFile": "Click or drag file to this area to upload", diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index 241a96048b..9f793e9501 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -377,7 +377,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { } async fn duplicate_view(&self, view_id: &str) -> Result { - let delta_bytes = self.0.duplicate_database(view_id).await?; + let delta_bytes = self.0.get_database_json_bytes(view_id).await?; Ok(Bytes::from(delta_bytes)) } diff --git a/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs index b9fc85387f..981140e041 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs @@ -4,6 +4,9 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; pub enum DatabaseExportDataType { #[default] CSV = 0, + + // DatabaseData + RawDatabaseData = 1, } #[derive(Debug, ProtoBuf, Default, Clone)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 2a497e0e30..43f5010c3b 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -1027,6 +1027,20 @@ pub(crate) async fn export_csv_handler( }) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn export_raw_database_data_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let view_id = data.into_inner().value; + let data = manager.get_database_json_string(&view_id).await?; + data_result_ok(DatabaseExportDataPB { + export_type: DatabaseExportDataType::RawDatabaseData, + data, + }) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_snapshots_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 5b0db9d9ed..bbe01c2fe1 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -77,6 +77,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::CreateDatabaseView, create_database_view) // Export .event(DatabaseEvent::ExportCSV, export_csv_handler) + .event(DatabaseEvent::ExportRawDatabaseData, export_raw_database_data_handler) .event(DatabaseEvent::GetDatabaseSnapshots, get_snapshots_handler) // Field settings .event(DatabaseEvent::GetFieldSettings, get_field_settings_handler) @@ -385,4 +386,7 @@ pub enum DatabaseEvent { #[event(input = "DatabaseViewIdPB", output = "RepeatedRowMetaPB")] GetAllRows = 177, + + #[event(input = "DatabaseViewIdPB", output = "DatabaseExportDataPB")] + ExportRawDatabaseData = 178, } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 0c4b13cab8..3885a8d0e6 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -345,7 +345,7 @@ impl DatabaseManager { Ok(()) } - pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult> { + pub async fn get_database_json_bytes(&self, view_id: &str) -> FlowyResult> { let lock = self.workspace_database()?; let wdb = lock.read().await; let data = wdb.get_database_data(view_id).await?; @@ -353,6 +353,14 @@ impl DatabaseManager { Ok(json_bytes) } + pub async fn get_database_json_string(&self, view_id: &str) -> FlowyResult { + let lock = self.workspace_database()?; + let wdb = lock.read().await; + let data = wdb.get_database_data(view_id).await?; + let json_string = serde_json::to_string(&data)?; + Ok(json_string) + } + /// Create a new database with the given data that can be deserialized to [DatabaseData]. #[tracing::instrument(level = "trace", skip_all, err)] pub async fn create_database_with_database_data(