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
This commit is contained in:
Lucas.Xu 2024-03-04 11:24:25 +07:00 committed by GitHub
parent 6b05be2362
commit 8944edf75f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 229 additions and 27 deletions

View File

@ -1,13 +1,15 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.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/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.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:appflowy/util/share_log_files.dart';
import 'package:easy_localization/easy_localization.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 'package:package_info_plus/package_info_plus.dart';
import 'widgets/widgets.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<FlowyCacheManager>().clearAllCache();
},
);
},
),
], ],
), ),
); );

View File

@ -59,10 +59,8 @@ class _ResizableImageState extends State<ResizableImage> {
imageWidth = widget.width; imageWidth = widget.width;
if (widget.type == CustomImageType.internal) {
_userProfilePB = context.read<DocumentBloc>().state.userProfilePB; _userProfilePB = context.read<DocumentBloc>().state.userProfilePB;
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -0,0 +1,61 @@
import 'package:appflowy_backend/log.dart';
import 'package:path_provider/path_provider.dart';
class FlowyCacheManager {
final _caches = <ICache>[];
// 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<void> clearAllCache() async {
try {
for (final cache in _caches) {
await cache.clearAll();
}
Log.info('Cache cleared');
} catch (e) {
Log.error(e);
}
}
Future<int> 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<int> cacheSize();
Future<void> clearAll();
}
class TemporaryDirectoryCache implements ICache {
@override
Future<int> cacheSize() async {
final tmpDir = await getTemporaryDirectory();
final tmpDirStat = await tmpDir.stat();
return tmpDirStat.size;
}
@override
Future<void> clearAll() async {
final tmpDir = await getTemporaryDirectory();
await tmpDir.delete(recursive: true);
}
}

View File

@ -39,8 +39,10 @@ class FlowyNetworkImage extends StatelessWidget {
assert(userProfilePB != null && userProfilePB!.token.isNotEmpty); assert(userProfilePB != null && userProfilePB!.token.isNotEmpty);
} }
final manager = CustomImageCacheManager();
return CachedNetworkImage( return CachedNetworkImage(
cacheManager: CustomImageCacheManager(), cacheManager: manager,
httpHeaders: _header(), httpHeaders: _header(),
imageUrl: url, imageUrl: url,
fit: fit, fit: fit,
@ -50,6 +52,12 @@ class FlowyNetworkImage extends StatelessWidget {
errorWidget: (context, url, error) => errorWidget: (context, url, error) =>
errorWidgetBuilder?.call(context, url, error) ?? errorWidgetBuilder?.call(context, url, error) ??
const SizedBox.shrink(), const SizedBox.shrink(),
errorListener: (value) {
// try to clear the image cache.
manager.removeFile(url);
Log.error(value.toString());
},
); );
} }

View File

@ -1,6 +1,9 @@
import 'package:appflowy/shared/appflowy_cache_manager.dart';
import 'package:flutter_cache_manager/flutter_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)); CustomImageCacheManager._() : super(Config(key));
factory CustomImageCacheManager() => _instance; factory CustomImageCacheManager() => _instance;
@ -8,4 +11,16 @@ class CustomImageCacheManager extends CacheManager with ImageCacheManager {
static final CustomImageCacheManager _instance = CustomImageCacheManager._(); static final CustomImageCacheManager _instance = CustomImageCacheManager._();
static const key = 'appflowy_image_cache'; static const key = 'appflowy_image_cache';
@override
Future<int> 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<void> clearAll() async {
await emptyCache();
}
} }

View File

@ -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/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_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/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/startup.dart';
import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart';
import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart'; import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart';
@ -128,6 +130,12 @@ void _resolveCommonService(
getIt.registerFactory<BaseAppearance>( getIt.registerFactory<BaseAppearance>(
() => PlatformExtension.isMobile ? MobileAppearance() : DesktopAppearance(), () => PlatformExtension.isMobile ? MobileAppearance() : DesktopAppearance(),
); );
getIt.registerFactory<FlowyCacheManager>(
() => FlowyCacheManager()
..registerCache(TemporaryDirectoryCache())
..registerCache(CustomImageCacheManager()),
);
} }
void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { void _resolveUserDeps(GetIt getIt, IntegrationMode mode) {

View File

@ -1,11 +1,11 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import '../../../../generated/locale_keys.g.dart'; import '../../../../../generated/locale_keys.g.dart';
class SettingsExportFileWidget extends StatefulWidget { class SettingsExportFileWidget extends StatefulWidget {
const SettingsExportFileWidget({ const SettingsExportFileWidget({

View File

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

View File

@ -1,8 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.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/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.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:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import '../../../../generated/locale_keys.g.dart'; import '../../../../../generated/locale_keys.g.dart';
import '../../../../startup/startup.dart'; import '../../../../../startup/startup.dart';
import '../../../../startup/tasks/prelude.dart'; import '../../../../../startup/tasks/prelude.dart';
class SettingsFileLocationCustomizer extends StatefulWidget { class SettingsFileLocationCustomizer extends StatefulWidget {
const SettingsFileLocationCustomizer({ const SettingsFileLocationCustomizer({
@ -262,7 +261,7 @@ class _RecoverDefaultStorageButtonState
tooltipText: LocaleKeys.settings_files_recoverLocationTooltips.tr(), tooltipText: LocaleKeys.settings_files_recoverLocationTooltips.tr(),
icon: const FlowySvg( icon: const FlowySvg(
FlowySvgs.restore_s, FlowySvgs.restore_s,
size: Size.square(24), size: Size.square(20),
), ),
onPressed: () async { onPressed: () async {
// reset to the default directory and reload app // reset to the default directory and reload app

View File

@ -18,7 +18,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../../../../generated/locale_keys.g.dart'; import '../../../../../generated/locale_keys.g.dart';
class FileExporterWidget extends StatefulWidget { class FileExporterWidget extends StatefulWidget {
const FileExporterWidget({super.key}); const FileExporterWidget({super.key});

View File

@ -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/files/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/files/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/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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -19,15 +21,15 @@ class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
// disable export data for v0.2.0 in release mode. // disable export data for v0.2.0 in release mode.
if (kDebugMode) const SettingsExportFileWidget(), if (kDebugMode) const SettingsExportFileWidget(),
const ImportAppFlowyData(), const ImportAppFlowyData(),
// clear the cache
const SettingsFileCacheWidget(),
]; ];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.separated( return SeparatedColumn(
shrinkWrap: true, separatorBuilder: () => const Divider(),
itemBuilder: (context, index) => _items[index], children: _items,
separatorBuilder: (context, index) => const Divider(),
itemCount: _items.length,
); );
} }
} }

View File

@ -457,7 +457,11 @@
"recoverLocationTooltips": "Reset to AppFlowy's default data directory", "recoverLocationTooltips": "Reset to AppFlowy's default data directory",
"exportFileSuccess": "Export file successfully!", "exportFileSuccess": "Export file successfully!",
"exportFileFail": "Export file failed!", "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": { "user": {
"name": "Name", "name": "Name",