From 8944edf75f897532de3cb14767ab1e57ed0b4415 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 4 Mar 2024 11:24:25 +0700 Subject: [PATCH] feat: support clearing caches and fix unable to load image (#4809) * feat: support clearing caches * fix: try to clear the image cache when loading failed. * feat: clear cache on mobile * chore: add error log --- .../setting/support_setting_group.dart | 31 ++++++- .../image/resizeable_image.dart | 4 +- .../lib/shared/appflowy_cache_manager.dart | 61 ++++++++++++++ .../lib/shared/appflowy_network_image.dart | 10 ++- .../shared/custom_image_cache_manager.dart | 17 +++- .../lib/startup/deps_resolver.dart | 8 ++ ...etting_file_import_appflowy_data_view.dart | 0 .../settings_export_file_widget.dart | 6 +- .../files/settings_file_cache_widget.dart | 80 +++++++++++++++++++ ...settings_file_customize_location_view.dart | 13 ++- .../settings_file_exporter_widget.dart | 2 +- .../widgets/settings_file_system_view.dart | 18 +++-- frontend/resources/translations/en.json | 6 +- 13 files changed, 229 insertions(+), 27 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/shared/appflowy_cache_manager.dart rename frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/{ => files}/setting_file_import_appflowy_data_view.dart (100%) rename frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/{ => files}/settings_export_file_widget.dart (95%) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart rename frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/{ => files}/settings_file_customize_location_view.dart (97%) rename frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/{ => files}/settings_file_exporter_widget.dart (99%) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart index 89e306b18b..1fd3fed7a6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart @@ -1,13 +1,15 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/shared/appflowy_cache_manager.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/share_log_files.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'widgets/widgets.dart'; @@ -51,6 +53,31 @@ class SupportSettingGroup extends StatelessWidget { ); }, ), + MobileSettingItem( + name: LocaleKeys.settings_files_clearCache.tr(), + trailing: const Icon( + Icons.chevron_right, + ), + onTap: () async { + await showFlowyMobileConfirmDialog( + context, + title: FlowyText( + LocaleKeys.settings_files_areYouSureToClearCache.tr(), + maxLines: 2, + ), + content: FlowyText( + LocaleKeys.settings_files_clearCacheDesc.tr(), + fontSize: 12, + maxLines: 4, + ), + actionButtonTitle: LocaleKeys.button_yes.tr(), + actionButtonColor: Theme.of(context).colorScheme.error, + onActionButtonPressed: () async { + await getIt().clearAllCache(); + }, + ); + }, + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 1e2305137d..055ae17605 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -59,9 +59,7 @@ class _ResizableImageState extends State { imageWidth = widget.width; - if (widget.type == CustomImageType.internal) { - _userProfilePB = context.read().state.userProfilePB; - } + _userProfilePB = context.read().state.userProfilePB; } @override diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_cache_manager.dart b/frontend/appflowy_flutter/lib/shared/appflowy_cache_manager.dart new file mode 100644 index 0000000000..be62cc44be --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/appflowy_cache_manager.dart @@ -0,0 +1,61 @@ +import 'package:appflowy_backend/log.dart'; +import 'package:path_provider/path_provider.dart'; + +class FlowyCacheManager { + final _caches = []; + + // if you add a new cache, you should register it here. + void registerCache(ICache cache) { + _caches.add(cache); + } + + void unregisterAllCache(ICache cache) { + _caches.clear(); + } + + Future clearAllCache() async { + try { + for (final cache in _caches) { + await cache.clearAll(); + } + + Log.info('Cache cleared'); + } catch (e) { + Log.error(e); + } + } + + Future getCacheSize() async { + try { + int tmpDirSize = 0; + for (final cache in _caches) { + tmpDirSize += await cache.cacheSize(); + } + Log.info('Cache size: $tmpDirSize'); + return tmpDirSize; + } catch (e) { + Log.error(e); + return 0; + } + } +} + +abstract class ICache { + Future cacheSize(); + Future clearAll(); +} + +class TemporaryDirectoryCache implements ICache { + @override + Future cacheSize() async { + final tmpDir = await getTemporaryDirectory(); + final tmpDirStat = await tmpDir.stat(); + return tmpDirStat.size; + } + + @override + Future clearAll() async { + final tmpDir = await getTemporaryDirectory(); + await tmpDir.delete(recursive: true); + } +} diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart index 72c6be583e..d37b2e0838 100644 --- a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart +++ b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart @@ -39,8 +39,10 @@ class FlowyNetworkImage extends StatelessWidget { assert(userProfilePB != null && userProfilePB!.token.isNotEmpty); } + final manager = CustomImageCacheManager(); + return CachedNetworkImage( - cacheManager: CustomImageCacheManager(), + cacheManager: manager, httpHeaders: _header(), imageUrl: url, fit: fit, @@ -50,6 +52,12 @@ class FlowyNetworkImage extends StatelessWidget { errorWidget: (context, url, error) => errorWidgetBuilder?.call(context, url, error) ?? const SizedBox.shrink(), + errorListener: (value) { + // try to clear the image cache. + manager.removeFile(url); + + Log.error(value.toString()); + }, ); } diff --git a/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart b/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart index 5e5e677193..b0624301c1 100644 --- a/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart +++ b/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart @@ -1,6 +1,9 @@ +import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -class CustomImageCacheManager extends CacheManager with ImageCacheManager { +class CustomImageCacheManager extends CacheManager + with ImageCacheManager + implements ICache { CustomImageCacheManager._() : super(Config(key)); factory CustomImageCacheManager() => _instance; @@ -8,4 +11,16 @@ class CustomImageCacheManager extends CacheManager with ImageCacheManager { static final CustomImageCacheManager _instance = CustomImageCacheManager._(); static const key = 'appflowy_image_cache'; + + @override + Future cacheSize() async { + // https://github.com/Baseflow/flutter_cache_manager/issues/239#issuecomment-719475429 + // this package does not provide a way to get the cache size + return 0; + } + + @override + Future clearAll() async { + await emptyCache(); + } } diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index adca53db85..d9e1692dd7 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -6,6 +6,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart'; import 'package:appflowy/plugins/trash/application/prelude.dart'; +import 'package:appflowy/shared/appflowy_cache_manager.dart'; +import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart'; @@ -128,6 +130,12 @@ void _resolveCommonService( getIt.registerFactory( () => PlatformExtension.isMobile ? MobileAppearance() : DesktopAppearance(), ); + + getIt.registerFactory( + () => FlowyCacheManager() + ..registerCache(TemporaryDirectoryCache()) + ..registerCache(CustomImageCacheManager()), + ); } void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart similarity index 95% rename from frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart index d97b8a5631..1c6441e90b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart @@ -1,11 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart'; -import 'package:flutter/material.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'; +import '../../../../../generated/locale_keys.g.dart'; class SettingsExportFileWidget extends StatefulWidget { const SettingsExportFileWidget({ 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 new file mode 100644 index 0000000000..011b7ece9f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart @@ -0,0 +1,80 @@ +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/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart similarity index 97% rename from frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart index 6f2a3c556f..5fdc8ff7cc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart @@ -1,8 +1,5 @@ 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'; @@ -12,12 +9,14 @@ 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/material.dart'; +import 'package:flutter/services.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'; +import '../../../../../generated/locale_keys.g.dart'; +import '../../../../../startup/startup.dart'; +import '../../../../../startup/tasks/prelude.dart'; class SettingsFileLocationCustomizer extends StatefulWidget { const SettingsFileLocationCustomizer({ @@ -262,7 +261,7 @@ class _RecoverDefaultStorageButtonState tooltipText: LocaleKeys.settings_files_recoverLocationTooltips.tr(), icon: const FlowySvg( FlowySvgs.restore_s, - size: Size.square(24), + size: Size.square(20), ), onPressed: () async { // reset to the default directory and reload app diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart similarity index 99% rename from frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart index efd789ed99..ea1cd98933 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; -import '../../../../generated/locale_keys.g.dart'; +import '../../../../../generated/locale_keys.g.dart'; class FileExporterWidget extends StatefulWidget { const FileExporterWidget({super.key}); 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 index 31aa7171bd..1def9a7b2b 100644 --- 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 @@ -1,6 +1,8 @@ -import 'package:appflowy/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_export_file_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.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:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -19,15 +21,15 @@ class _SettingsFileSystemViewState extends State { // disable export data for v0.2.0 in release mode. if (kDebugMode) const SettingsExportFileWidget(), const ImportAppFlowyData(), + // clear the cache + const SettingsFileCacheWidget(), ]; @override Widget build(BuildContext context) { - return ListView.separated( - shrinkWrap: true, - itemBuilder: (context, index) => _items[index], - separatorBuilder: (context, index) => const Divider(), - itemCount: _items.length, + return SeparatedColumn( + separatorBuilder: () => const Divider(), + children: _items, ); } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 71e2ea9909..fdf804c38e 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -457,7 +457,11 @@ "recoverLocationTooltips": "Reset to AppFlowy's default data directory", "exportFileSuccess": "Export file successfully!", "exportFileFail": "Export file failed!", - "export": "Export" + "export": "Export", + "clearCache": "Clear cache", + "clearCacheDesc": "Clear the cache including the images, fonts, and other temporary files. This will not delete your data.", + "areYouSureToClearCache": "Are you sure to clear the cache?", + "clearCacheSuccess": "Cache cleared successfully!" }, "user": { "name": "Name",