From 8273d66c501ebf8045161579bad040b94e89ff2a Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 13 May 2024 09:45:56 +0200 Subject: [PATCH] feat: settings manage data (#5265) * feat: settings manage data page * fix: changes after merge * test: fix failing integration test * fix: missing localizations --- .../uncategorized/switch_folder_test.dart | 2 +- .../integration_test/shared/settings.dart | 8 +- .../screens/encrypt_secret_screen.dart | 24 +- .../settings/settings_dialog_bloc.dart | 2 +- .../pages/settings_manage_data_view.dart | 493 ++++++++++++++++++ .../settings/settings_dialog.dart | 6 +- .../settings/shared/setting_action.dart | 17 +- ...etting_file_import_appflowy_data_view.dart | 167 ------ .../files/settings_export_file_widget.dart | 7 +- .../files/settings_file_cache_widget.dart | 80 --- ...settings_file_customize_location_view.dart | 285 ---------- .../widgets/settings_file_system_view.dart | 32 -- .../settings/widgets/settings_menu.dart | 6 +- frontend/resources/translations/en.json | 48 +- 14 files changed, 579 insertions(+), 598 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart index 68db03d429..b9e1303279 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart @@ -101,7 +101,7 @@ void main() { // open settings and restore the location await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.files); + await tester.openSettingsPage(SettingsPage.manageData); await tester.restoreLocation(); expect( diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index 9ebfbe24e3..dd13bd088f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -39,10 +39,14 @@ extension AppFlowySettings on WidgetTester { /// Restore the AppFlowy data storage location Future restoreLocation() async { - final button = - find.byTooltip(LocaleKeys.settings_files_recoverLocationTooltips.tr()); + final button = find.text(LocaleKeys.settings_common_reset.tr()); expect(button, findsOneWidget); await tapButton(button); + await pumpAndSettle(); + + final confirmButton = find.text(LocaleKeys.button_confirm.tr()); + expect(confirmButton, findsOneWidget); + await tapButton(confirmButton); return; } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart index 1edd20b671..f0b79ed9d2 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/helpers/helpers.dart'; @@ -6,7 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../application/encrypt_secret_bloc.dart'; @@ -98,23 +99,20 @@ class _EncryptSecretScreenState extends State { controller: _textEditingController, hintText: LocaleKeys.settings_menu_inputTextFieldHint.tr(), - onChanged: (p0) {}, + onChanged: (_) {}, ), ), OkCancelButton( alignment: MainAxisAlignment.end, - onOkPressed: () { - context.read().add( - EncryptSecretEvent.setEncryptSecret( - _textEditingController.text, + onOkPressed: () => + context.read().add( + EncryptSecretEvent.setEncryptSecret( + _textEditingController.text, + ), ), - ); - }, - onCancelPressed: () { - context.read().add( - const EncryptSecretEvent.cancelInputSecret(), - ); - }, + onCancelPressed: () => context + .read() + .add(const EncryptSecretEvent.cancelInputSecret()), mode: TextButtonMode.normal, ), const VSpace(6), diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 17a9936398..02aabc6c57 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -12,8 +12,8 @@ enum SettingsPage { // NEW account, workspace, + manageData, // OLD - files, notifications, cloud, shortcuts, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart new file mode 100644 index 0000000000..17d76b4fe1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -0,0 +1,493 @@ +import 'dart:async'; + +import 'package:flutter/foundation.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/appflowy_cache_manager.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/rust_sdk.dart'; +import 'package:appflowy/workspace/application/settings/setting_file_importer_bloc.dart'; +import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.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/spacing.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class SettingsManageDataView extends StatelessWidget { + const SettingsManageDataView({super.key, required this.userProfile}); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => SettingsLocationCubit(), + child: BlocBuilder( + builder: (context, state) { + return SettingsBody( + title: LocaleKeys.settings_manageDataPage_title.tr(), + description: LocaleKeys.settings_manageDataPage_description.tr(), + children: [ + SettingsCategory( + title: + LocaleKeys.settings_manageDataPage_dataStorage_title.tr(), + tooltip: + LocaleKeys.settings_manageDataPage_dataStorage_tooltip.tr(), + actions: [ + if (state.mapOrNull(didReceivedPath: (_) => true) == true) + SettingAction( + icon: const FlowySvg(FlowySvgs.restore_s), + label: LocaleKeys.settings_common_reset.tr(), + onPressed: () => SettingsAlertDialog( + title: LocaleKeys + .settings_manageDataPage_dataStorage_resetDialog_title + .tr(), + subtitle: LocaleKeys + .settings_manageDataPage_dataStorage_resetDialog_description + .tr(), + implyLeading: true, + confirm: () async { + final directory = + await appFlowyApplicationDataDirectory(); + final path = directory.path; + if (!context.mounted || + state.mapOrNull(didReceivedPath: (e) => e.path) == + path) { + return; + } + + await context + .read() + .resetDataStoragePathToApplicationDefault(); + await runAppFlowy(isAnon: true); + + if (context.mounted) Navigator.of(context).pop(); + }, + ).show(context), + ), + ], + children: state + .map( + initial: (_) => [const CircularProgressIndicator()], + didReceivedPath: (event) => [ + _CurrentPath(path: event.path), + _DataPathActions(currentPath: event.path), + ], + ) + .toList(), + ), + SettingsCategory( + title: LocaleKeys.settings_manageDataPage_importData_title.tr(), + tooltip: + LocaleKeys.settings_manageDataPage_importData_tooltip.tr(), + children: const [_ImportDataField()], + ), + if (kDebugMode) ...[ + SettingsCategory( + title: LocaleKeys.settings_files_exportData.tr(), + children: const [SettingsExportFileWidget()], + ), + ], + SettingsCategory( + title: LocaleKeys.settings_manageDataPage_cache_title.tr(), + children: [ + SingleSettingAction( + labelMaxLines: 4, + label: LocaleKeys.settings_manageDataPage_cache_description + .tr(), + buttonLabel: + LocaleKeys.settings_manageDataPage_cache_title.tr(), + onPressed: () { + SettingsAlertDialog( + title: LocaleKeys + .settings_manageDataPage_cache_dialog_title + .tr(), + subtitle: LocaleKeys + .settings_manageDataPage_cache_dialog_description + .tr(), + confirm: () async { + await getIt().clearAllCache(); + if (context.mounted) { + showSnackBarMessage( + context, + LocaleKeys + .settings_manageDataPage_cache_dialog_successHint + .tr(), + ); + Navigator.of(context).pop(); + } + }, + ).show(context); + }, + ), + ], + ), + // Uncomment if we need to enable encryption + // if (userProfile.authenticator == AuthenticatorPB.Supabase) ...[ + // const SettingsCategorySpacer(), + // BlocProvider( + // create: (_) => EncryptSecretBloc(user: userProfile), + // child: SettingsCategory( + // title: LocaleKeys.settings_manageDataPage_encryption_title + // .tr(), + // tooltip: LocaleKeys + // .settings_manageDataPage_encryption_tooltip + // .tr(), + // description: userProfile.encryptionType == + // EncryptionTypePB.NoEncryption + // ? LocaleKeys + // .settings_manageDataPage_encryption_descriptionNoEncryption + // .tr() + // : LocaleKeys + // .settings_manageDataPage_encryption_descriptionEncrypted + // .tr(), + // children: [_EncryptDataSetting(userProfile: userProfile)], + // ), + // ), + // ], + ], + ); + }, + ), + ); + } +} + +// class _EncryptDataSetting extends StatelessWidget { +// const _EncryptDataSetting({required this.userProfile}); + +// final UserProfilePB userProfile; + +// @override +// Widget build(BuildContext context) { +// return BlocProvider.value( +// value: context.read(), +// child: BlocBuilder( +// builder: (context, state) { +// if (state.loadingState?.isLoading() == true) { +// return const Row( +// children: [ +// SizedBox( +// width: 20, +// height: 20, +// child: CircularProgressIndicator( +// strokeWidth: 3, +// ), +// ), +// HSpace(16), +// FlowyText.medium( +// 'Encrypting data...', +// fontSize: 14, +// ), +// ], +// ); +// } + +// if (userProfile.encryptionType == EncryptionTypePB.NoEncryption) { +// return Row( +// children: [ +// SizedBox( +// height: 42, +// child: FlowyTextButton( +// LocaleKeys.settings_manageDataPage_encryption_action.tr(), +// padding: const EdgeInsets.symmetric( +// horizontal: 24, +// vertical: 12, +// ), +// fontWeight: FontWeight.w600, +// radius: BorderRadius.circular(12), +// fillColor: Theme.of(context).colorScheme.primary, +// hoverColor: const Color(0xFF005483), +// fontHoverColor: Colors.white, +// onPressed: () => SettingsAlertDialog( +// title: LocaleKeys +// .settings_manageDataPage_encryption_dialog_title +// .tr(), +// subtitle: LocaleKeys +// .settings_manageDataPage_encryption_dialog_description +// .tr(), +// confirmLabel: LocaleKeys +// .settings_manageDataPage_encryption_dialog_title +// .tr(), +// implyLeading: true, +// // Generate a secret one time for the user +// confirm: () => context +// .read() +// .add(const EncryptSecretEvent.setEncryptSecret('')), +// ).show(context), +// ), +// ), +// ], +// ); +// } +// // Show encryption secret for copy/save +// return const SizedBox.shrink(); +// }, +// ), +// ); +// } +// } + +class _ImportDataField extends StatefulWidget { + const _ImportDataField(); + + @override + State<_ImportDataField> createState() => _ImportDataFieldState(); +} + +class _ImportDataFieldState extends State<_ImportDataField> { + final _fToast = FToast(); + + @override + void initState() { + super.initState(); + _fToast.init(context); + } + + @override + void dispose() { + _fToast.removeQueuedCustomToasts(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SettingFileImportBloc(), + child: BlocConsumer( + listenWhen: (previous, current) => + previous.successOrFail != current.successOrFail, + listener: (_, state) => state.successOrFail?.fold( + (_) => _showToast(LocaleKeys.settings_menu_importSuccess.tr()), + (_) => _showToast(LocaleKeys.settings_menu_importFailed.tr()), + ), + builder: (context, state) { + return DottedBorder( + radius: const Radius.circular(8), + dashPattern: const [2, 2], + borderType: BorderType.RRect, + color: Theme.of(context).colorScheme.primary, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // When dragging files are enabled + // FlowyText.regular('Drag file here or'), + // const VSpace(8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 42, + child: FlowyTextButton( + LocaleKeys.settings_manageDataPage_importData_action + .tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () async { + final path = await getIt() + .getDirectoryPath(); + if (path == null || !context.mounted) { + return; + } + + context.read().add( + SettingFileImportEvent + .importAppFlowyDataFolder( + path, + ), + ); + }, + ), + ), + ], + ), + const VSpace(8), + FlowyText.regular( + LocaleKeys.settings_manageDataPage_importData_description + .tr(), + // 'Supported filetypes:\nCSV, Notion, Text, and Markdown', + maxLines: 3, + lineHeight: 1.5, + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + }, + ), + ); + } + + void _showToast(String message) { + _fToast.showToast( + child: FlowyMessageToast(message: message), + gravity: ToastGravity.CENTER, + ); + } +} + +class _CurrentPath extends StatefulWidget { + const _CurrentPath({required this.path}); + + final String path; + + @override + State<_CurrentPath> createState() => _CurrentPathState(); +} + +class _CurrentPathState extends State<_CurrentPath> { + Timer? linkCopiedTimer; + bool showCopyMessage = false; + + @override + void dispose() { + linkCopiedTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Expanded( + child: Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (_) => _copyLink(widget.path), + child: FlowyHover( + style: const HoverStyle.transparent(), + resetHoverOnRebuild: false, + builder: (_, isHovering) => FlowyText.regular( + widget.path, + lineHeight: 1.5, + maxLines: 2, + overflow: TextOverflow.ellipsis, + decoration: isHovering ? TextDecoration.underline : null, + color: const Color(0xFF005483), + ), + ), + ), + ), + const HSpace(8), + showCopyMessage + ? SizedBox( + height: 36, + child: FlowyTextButton( + LocaleKeys + .settings_manageDataPage_dataStorage_actions_copiedHint + .tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontWeight: FontWeight.w500, + radius: BorderRadius.circular(12), + fillColor: AFThemeExtension.of(context).tint7, + hoverColor: AFThemeExtension.of(context).tint7, + ), + ) + : Padding( + padding: const EdgeInsets.only(left: 100), + child: SettingAction( + tooltip: LocaleKeys + .settings_manageDataPage_dataStorage_actions_copy + .tr(), + icon: const FlowySvg( + FlowySvgs.copy_s, + size: Size.square(24), + ), + onPressed: () => _copyLink(widget.path), + ), + ), + ], + ), + ], + ); + } + + void _copyLink(String? path) { + AppFlowyClipboard.setData(text: path); + setState(() => showCopyMessage = true); + linkCopiedTimer?.cancel(); + linkCopiedTimer = Timer( + const Duration(milliseconds: 300), + () => mounted ? setState(() => showCopyMessage = false) : null, + ); + } +} + +class _DataPathActions extends StatelessWidget { + const _DataPathActions({required this.currentPath}); + + final String currentPath; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + height: 42, + child: FlowyTextButton( + LocaleKeys.settings_manageDataPage_dataStorage_actions_change.tr(), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () async { + final path = await getIt().getDirectoryPath(); + if (!context.mounted || path == null || currentPath == path) { + return; + } + + await context.read().setCustomPath(path); + await runAppFlowy(isAnon: true); + + if (context.mounted) Navigator.of(context).pop(); + }, + ), + ), + const HSpace(16), + SettingAction( + tooltip: LocaleKeys + .settings_manageDataPage_dataStorage_actions_openTooltip + .tr(), + label: + LocaleKeys.settings_manageDataPage_dataStorage_actions_open.tr(), + icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(16)), + onPressed: () => afLaunchUrlString('file://$currentPath'), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 2a6e2bfbee..9153215c79 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -79,8 +79,8 @@ class SettingsDialog extends StatelessWidget { ); case SettingsPage.workspace: return SettingsWorkspaceView(userProfile: user); - case SettingsPage.files: - return const SettingsFileSystemView(); + case SettingsPage.manageData: + return SettingsManageDataView(userProfile: user); case SettingsPage.notifications: return const SettingsNotificationsView(); case SettingsPage.cloud: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_action.dart index 41c677f7bd..e4551d1c2c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_action.dart @@ -21,11 +21,7 @@ class SettingAction extends StatelessWidget { @override Widget build(BuildContext context) { - final iconWidget = tooltip != null && tooltip!.isNotEmpty - ? FlowyTooltip(message: tooltip, child: icon) - : icon; - - return GestureDetector( + final child = GestureDetector( behavior: HitTestBehavior.opaque, onTap: onPressed, child: SizedBox( @@ -36,7 +32,7 @@ class SettingAction extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Row( children: [ - iconWidget, + icon, if (label != null) ...[ const HSpace(4), FlowyText.regular(label!), @@ -47,5 +43,14 @@ class SettingAction extends StatelessWidget { ), ), ); + + if (tooltip != null) { + return FlowyTooltip( + message: tooltip!, + child: child, + ); + } + + return child; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart deleted file mode 100644 index 20498752b9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/settings/setting_file_importer_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/toast.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_bloc/flutter_bloc.dart'; -import 'package:fluttertoast/fluttertoast.dart'; - -class ImportAppFlowyData extends StatefulWidget { - const ImportAppFlowyData({super.key}); - - @override - State createState() => _ImportAppFlowyDataState(); -} - -class _ImportAppFlowyDataState extends State { - final _fToast = FToast(); - @override - void initState() { - super.initState(); - _fToast.init(context); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SettingFileImportBloc(), - child: BlocListener( - listener: (context, state) { - state.successOrFail?.fold( - (_) { - _showToast(LocaleKeys.settings_menu_importSuccess.tr()); - }, - (_) { - _showToast(LocaleKeys.settings_menu_importFailed.tr()); - }, - ); - }, - child: BlocBuilder( - builder: (context, state) { - final List children = [ - const ImportAppFlowyDataButton(), - const VSpace(6), - ]; - - if (state.loadingState.isLoading()) { - children.add(const AppFlowyDataImportingTip()); - } else { - children.add(const AppFlowyDataImportTip()); - } - - return Column(children: children); - }, - ), - ), - ); - } - - void _showToast(String message) { - _fToast.showToast( - child: FlowyMessageToast(message: message), - gravity: ToastGravity.CENTER, - ); - } -} - -class AppFlowyDataImportTip extends StatelessWidget { - const AppFlowyDataImportTip({super.key}); - - final url = "https://docs.appflowy.io/docs/appflowy/product/data-storage"; - - @override - Widget build(BuildContext context) { - return Opacity( - opacity: 0.6, - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_menu_importAppFlowyDataDescription.tr(), - style: Theme.of(context).textTheme.bodySmall!, - ), - TextSpan( - text: " ${LocaleKeys.settings_menu_importGuide.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString(url), - ), - ], - ), - ), - ); - } -} - -class ImportAppFlowyDataButton extends StatefulWidget { - const ImportAppFlowyDataButton({super.key}); - - @override - State createState() => - _ImportAppFlowyDataButtonState(); -} - -class _ImportAppFlowyDataButtonState extends State { - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - children: [ - SizedBox( - height: 40, - child: FlowyButton( - disable: state.loadingState.isLoading(), - text: - FlowyText(LocaleKeys.settings_menu_importAppFlowyData.tr()), - onTap: () async { - final path = - await getIt().getDirectoryPath(); - if (path == null || !context.mounted) { - return; - } - - context.read().add( - SettingFileImportEvent.importAppFlowyDataFolder(path), - ); - }, - ), - ), - if (state.loadingState.isLoading()) - const LinearProgressIndicator(minHeight: 1), - ], - ); - }, - ); - } -} - -class AppFlowyDataImportingTip extends StatelessWidget { - const AppFlowyDataImportingTip({super.key}); - - @override - Widget build(BuildContext context) { - return Opacity( - opacity: 0.6, - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_menu_importingAppFlowyDataTip.tr(), - style: Theme.of(context).textTheme.bodySmall!, - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart index 1c6441e90b..ed6c8949b7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart @@ -1,16 +1,15 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:styled_widget/styled_widget.dart'; import '../../../../../generated/locale_keys.g.dart'; class SettingsExportFileWidget extends StatefulWidget { - const SettingsExportFileWidget({ - super.key, - }); + const SettingsExportFileWidget({super.key}); @override State createState() => diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart deleted file mode 100644 index 011b7ece9f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/appflowy_cache_manager.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class SettingsFileCacheWidget extends StatelessWidget { - const SettingsFileCacheWidget({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 5.0), - child: FlowyText.medium( - LocaleKeys.settings_files_clearCache.tr(), - fontSize: 13, - overflow: TextOverflow.ellipsis, - ), - ), - const VSpace(8), - Opacity( - opacity: 0.6, - child: FlowyText( - LocaleKeys.settings_files_clearCacheDesc.tr(), - fontSize: 10, - maxLines: 3, - ), - ), - ], - ), - ), - const _ClearCacheButton(), - ], - ); - } -} - -class _ClearCacheButton extends StatelessWidget { - const _ClearCacheButton(); - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - tooltipText: LocaleKeys.settings_files_clearCache.tr(), - icon: FlowySvg( - FlowySvgs.delete_s, - size: const Size.square(18), - color: Theme.of(context).iconTheme.color, - ), - onPressed: () { - NavigatorAlertDialog( - title: LocaleKeys.settings_files_areYouSureToClearCache.tr(), - confirm: () async { - await getIt().clearAllCache(); - if (context.mounted) { - showSnackBarMessage( - context, - LocaleKeys.settings_files_clearCacheSuccess.tr(), - ); - } - }, - ).show(context); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart deleted file mode 100644 index e7cc9fff0d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/application/settings/settings_location_cubit.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:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:styled_widget/styled_widget.dart'; - -import '../../../../../generated/locale_keys.g.dart'; -import '../../../../../startup/startup.dart'; -import '../../../../../startup/tasks/prelude.dart'; - -class SettingsFileLocationCustomizer extends StatefulWidget { - const SettingsFileLocationCustomizer({super.key}); - - @override - State createState() => - SettingsFileLocationCustomizerState(); -} - -@visibleForTesting -class SettingsFileLocationCustomizerState - extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => SettingsLocationCubit(), - child: BlocBuilder( - builder: (context, state) { - return state.when( - initial: () => const Center( - child: CircularProgressIndicator(), - ), - didReceivedPath: (path) { - return Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - // display file paths. - _path(path), - - // display the icons - _buttons(path), - ], - ), - const VSpace(10), - IntrinsicHeight( - child: Opacity( - opacity: 0.6, - child: FlowyText.medium( - LocaleKeys.settings_menu_customPathPrompt.tr(), - maxLines: 13, - ), - ), - ), - ], - ); - }, - ); - }, - ), - ); - } - - Widget _path(String path) { - return Flexible( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - LocaleKeys.settings_files_defaultLocation.tr(), - fontSize: 13, - overflow: TextOverflow.visible, - ).padding(horizontal: 5), - const VSpace(5), - _CopyableText( - usingPath: path, - ), - ], - ), - ); - } - - Widget _buttons(String path) { - final List children = []; - children.addAll([ - Flexible( - child: _ChangeStoragePathButton( - usingPath: path, - ), - ), - const HSpace(10), - ]); - - children.add( - _OpenStorageButton( - usingPath: path, - ), - ); - - children.add( - _RecoverDefaultStorageButton( - usingPath: path, - ), - ); - - return Flexible( - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: children), - ); - } -} - -class _CopyableText extends StatelessWidget { - const _CopyableText({ - required this.usingPath, - }); - - final String usingPath; - - @override - Widget build(BuildContext context) { - return FlowyHover( - builder: (_, onHover) { - return GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: usingPath)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: FlowyText( - LocaleKeys.settings_files_pathCopiedSnackbar.tr(), - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ); - }, - child: Container( - height: 20, - padding: const EdgeInsets.symmetric(horizontal: 5), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: FlowyText.regular( - usingPath, - fontSize: 12, - overflow: TextOverflow.ellipsis, - ), - ), - if (onHover) ...[ - const HSpace(5), - FlowyText.regular( - LocaleKeys.settings_files_copy.tr(), - fontSize: 12, - color: Theme.of(context).colorScheme.primary, - ), - ], - ], - ), - ), - ); - }, - ); - } -} - -class _ChangeStoragePathButton extends StatefulWidget { - const _ChangeStoragePathButton({ - required this.usingPath, - }); - - final String usingPath; - - @override - State<_ChangeStoragePathButton> createState() => - _ChangeStoragePathButtonState(); -} - -class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> { - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.settings_files_changeLocationTooltips.tr(), - child: SecondaryTextButton( - LocaleKeys.settings_files_change.tr(), - mode: TextButtonMode.small, - onPressed: () async { - // pick the new directory and reload app - final path = await getIt().getDirectoryPath(); - if (path == null || widget.usingPath == path) { - return; - } - if (!context.mounted) { - return; - } - await context.read().setCustomPath(path); - await runAppFlowy(isAnon: true); - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - ); - } -} - -class _OpenStorageButton extends StatelessWidget { - const _OpenStorageButton({ - required this.usingPath, - }); - - final String usingPath; - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - tooltipText: LocaleKeys.settings_files_openCurrentDataFolder.tr(), - icon: FlowySvg( - FlowySvgs.open_folder_lg, - color: Theme.of(context).iconTheme.color, - ), - onPressed: () async { - final uri = Directory(usingPath).uri; - await afLaunchUrl(uri, context: context); - }, - ); - } -} - -class _RecoverDefaultStorageButton extends StatefulWidget { - const _RecoverDefaultStorageButton({ - required this.usingPath, - }); - - final String usingPath; - - @override - State<_RecoverDefaultStorageButton> createState() => - _RecoverDefaultStorageButtonState(); -} - -class _RecoverDefaultStorageButtonState - extends State<_RecoverDefaultStorageButton> { - @override - Widget build(BuildContext context) { - return FlowyIconButton( - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - tooltipText: LocaleKeys.settings_files_recoverLocationTooltips.tr(), - icon: const FlowySvg( - FlowySvgs.restore_s, - size: Size.square(20), - ), - onPressed: () async { - // reset to the default directory and reload app - final directory = await appFlowyApplicationDataDirectory(); - final path = directory.path; - if (widget.usingPath == path) { - return; - } - if (!context.mounted) { - return; - } - await context - .read() - .resetDataStoragePathToApplicationDefault(); - await runAppFlowy(isAnon: true); - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart deleted file mode 100644 index 7ca8eb3458..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class SettingsFileSystemView extends StatelessWidget { - const SettingsFileSystemView({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsBody( - title: LocaleKeys.settings_menu_files.tr(), - children: const [ - SettingsFileLocationCustomizer(), - SettingsCategorySpacer(), - if (kDebugMode) ...[ - SettingsExportFileWidget(), - ], - ImportAppFlowyData(), - SettingsCategorySpacer(), - SettingsFileCacheWidget(), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 901f930e94..0148db171b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -72,10 +72,10 @@ class SettingsMenu extends StatelessWidget { changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( - page: SettingsPage.files, + page: SettingsPage.manageData, selectedPage: currentPage, - label: LocaleKeys.settings_menu_files.tr(), - icon: const Icon(Icons.file_present_outlined), + label: LocaleKeys.settings_manageDataPage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_data_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 5e45d4bb0e..88ebedb546 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -417,6 +417,52 @@ "deleteWorkspace": "Delete workspace" } }, + "manageDataPage": { + "menuLabel": "Manage data", + "title": "Manage data", + "description": "Manage data local storage or Import your existing data into Appflowy. You can secure your data with end to end encryption.", + "dataStorage": { + "title": "File storage location", + "tooltip": "The location where your files are stored", + "actions": { + "change": "Change path", + "open": "Open folder", + "openTooltip": "Open current data folder location", + "copy": "Copy path", + "copiedHint": "Link copied!" + }, + "resetDialog": { + "title": "Are you sure?", + "description": "Resetting the path to the default data location will not delete your data. If you want to re-import your current data, you should copy the path of your current location first." + } + }, + "importData": { + "title": "Import data", + "tooltip": "Import data from AppFlowy backups/data folders", + "description": "Copy data from an external AppFlowy data folder and import it into the current AppFlowy data folder", + "action": "Browse folder" + }, + "encryption": { + "title": "Encryption", + "tooltip": "Manage how your data is stored and encrypted", + "descriptionNoEncryption": "Turning on encryption will encrypt all data. This can not be undone.", + "descriptionEncrypted": "Your data is encrypted.", + "action": "Encrypt data", + "dialog": { + "title": "Encrypt all your data?", + "description": "Encrypting all your data will keep your data safe and secure. This action can NOT be undone. Are you sure you want to continue?" + } + }, + "cache": { + "title": "Clear cache", + "description": "If you encounter issues with images not loading or fonts not displaying correctly, try clearing your cache. This action will not remove your user data.", + "dialog": { + "title": "Are you sure?", + "description": "Clearing the cache will cause images and fonts to be re-downloaded on load. This action will not remove or modify your data.", + "successHint": "Cache cleared!" + } + } + }, "common": { "reset": "Reset" }, @@ -1624,4 +1670,4 @@ "betaTooltip": "We currently only support searching for pages", "fromTrashHint": "From trash" } -} \ No newline at end of file +}