feat: settings manage data (#5265)

* feat: settings manage data page

* fix: changes after merge

* test: fix failing integration test

* fix: missing localizations
This commit is contained in:
Mathias Mogensen 2024-05-13 09:45:56 +02:00 committed by GitHub
parent 38fa9f7942
commit 8273d66c50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 579 additions and 598 deletions

View File

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

View File

@ -39,10 +39,14 @@ extension AppFlowySettings on WidgetTester {
/// Restore the AppFlowy data storage location
Future<void> 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;
}

View File

@ -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<EncryptSecretScreen> {
controller: _textEditingController,
hintText:
LocaleKeys.settings_menu_inputTextFieldHint.tr(),
onChanged: (p0) {},
onChanged: (_) {},
),
),
OkCancelButton(
alignment: MainAxisAlignment.end,
onOkPressed: () {
context.read<EncryptSecretBloc>().add(
EncryptSecretEvent.setEncryptSecret(
_textEditingController.text,
onOkPressed: () =>
context.read<EncryptSecretBloc>().add(
EncryptSecretEvent.setEncryptSecret(
_textEditingController.text,
),
),
);
},
onCancelPressed: () {
context.read<EncryptSecretBloc>().add(
const EncryptSecretEvent.cancelInputSecret(),
);
},
onCancelPressed: () => context
.read<EncryptSecretBloc>()
.add(const EncryptSecretEvent.cancelInputSecret()),
mode: TextButtonMode.normal,
),
const VSpace(6),

View File

@ -12,8 +12,8 @@ enum SettingsPage {
// NEW
account,
workspace,
manageData,
// OLD
files,
notifications,
cloud,
shortcuts,

View File

@ -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<SettingsLocationCubit>(
create: (_) => SettingsLocationCubit(),
child: BlocBuilder<SettingsLocationCubit, SettingsLocationState>(
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<SettingsLocationCubit>()
.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<FlowyCacheManager>().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<EncryptSecretBloc>.value(
// value: context.read<EncryptSecretBloc>(),
// child: BlocBuilder<EncryptSecretBloc, EncryptSecretState>(
// 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<EncryptSecretBloc>()
// .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<SettingFileImportBloc>(
create: (context) => SettingFileImportBloc(),
child: BlocConsumer<SettingFileImportBloc, SettingFileImportState>(
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<FilePickerService>()
.getDirectoryPath();
if (path == null || !context.mounted) {
return;
}
context.read<SettingFileImportBloc>().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<FilePickerService>().getDirectoryPath();
if (!context.mounted || path == null || currentPath == path) {
return;
}
await context.read<SettingsLocationCubit>().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'),
),
],
);
}
}

View File

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

View File

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

View File

@ -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<ImportAppFlowyData> createState() => _ImportAppFlowyDataState();
}
class _ImportAppFlowyDataState extends State<ImportAppFlowyData> {
final _fToast = FToast();
@override
void initState() {
super.initState();
_fToast.init(context);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SettingFileImportBloc(),
child: BlocListener<SettingFileImportBloc, SettingFileImportState>(
listener: (context, state) {
state.successOrFail?.fold(
(_) {
_showToast(LocaleKeys.settings_menu_importSuccess.tr());
},
(_) {
_showToast(LocaleKeys.settings_menu_importFailed.tr());
},
);
},
child: BlocBuilder<SettingFileImportBloc, SettingFileImportState>(
builder: (context, state) {
final List<Widget> 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>[
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<ImportAppFlowyDataButton> createState() =>
_ImportAppFlowyDataButtonState();
}
class _ImportAppFlowyDataButtonState extends State<ImportAppFlowyDataButton> {
@override
Widget build(BuildContext context) {
return BlocBuilder<SettingFileImportBloc, SettingFileImportState>(
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<FilePickerService>().getDirectoryPath();
if (path == null || !context.mounted) {
return;
}
context.read<SettingFileImportBloc>().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>[
TextSpan(
text: LocaleKeys.settings_menu_importingAppFlowyDataTip.tr(),
style: Theme.of(context).textTheme.bodySmall!,
),
],
),
),
);
}
}

View File

@ -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<SettingsExportFileWidget> createState() =>

View File

@ -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<FlowyCacheManager>().clearAllCache();
if (context.mounted) {
showSnackBarMessage(
context,
LocaleKeys.settings_files_clearCacheSuccess.tr(),
);
}
},
).show(context);
},
);
}
}

View File

@ -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<SettingsFileLocationCustomizer> createState() =>
SettingsFileLocationCustomizerState();
}
@visibleForTesting
class SettingsFileLocationCustomizerState
extends State<SettingsFileLocationCustomizer> {
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsLocationCubit>(
create: (_) => SettingsLocationCubit(),
child: BlocBuilder<SettingsLocationCubit, SettingsLocationState>(
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<Widget> 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<FilePickerService>().getDirectoryPath();
if (path == null || widget.usingPath == path) {
return;
}
if (!context.mounted) {
return;
}
await context.read<SettingsLocationCubit>().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<SettingsLocationCubit>()
.resetDataStoragePathToApplicationDefault();
await runAppFlowy(isAnon: true);
if (context.mounted) {
Navigator.of(context).pop();
}
},
);
}
}

View File

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

View File

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

View File

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