Files
xianyan/lib/features/mine/settings/presentation/image_cache_page.dart
Developer 9ea8d3d606 chore: 汇总批量提交的功能优化与bug修复
本次提交包含多项迭代优化和问题修复:
1. 新增缩略图图片组件、数字格式化工具类,补充多语言翻译类型与本地化支持
2. 优化底部导航栏主题色统一使用动态accent色值
3. 修复多处图表动画、路由跳转、API请求相关问题
4. 简化服务器公告文案,调整默认分屏状态为关闭
5. 新增安卓/iOS桌面快捷方式配置
6. 重构多处状态管理类使用SafeNotifierInit统一异常保护
7. 替换硬编码蓝色为主题色,更新版本号获取方式为动态读取
8. 优化缓存预加载逻辑,移除无用代码
9. 调整默认设置项,优化用户体验细节
2026-05-31 12:24:05 +08:00

734 lines
25 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 图片缓存管理页面
/// 创建时间: 2026-05-30
/// 更新时间: 2026-05-30
/// 作用: 图片缓存详情查看、按日期/类型智能分组、图片预览、自动清理策略
/// 上次更新: 全面接入Riverpod ProviderSliver布局批量操作错误边界动态过期
/// ============================================================
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:photo_view/photo_view.dart';
import '../../../../core/services/data/image_cache_metadata_service.dart';
import '../../../../core/theme/app_radius.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../l10n/translations.dart';
import '../../../../shared/widgets/adaptive/adaptive_back_button.dart';
import '../../../../shared/widgets/adaptive/responsive_layout.dart';
import '../../../../shared/widgets/feedback/app_toast.dart';
import '../providers/image_cache_provider.dart';
import 'image_cache_detail_page.dart';
import 'image_cache_grid.dart';
import 'image_cache_log_page.dart';
import 'image_cache_models.dart';
import 'image_cache_widgets.dart';
class ImageCachePage extends ConsumerWidget {
const ImageCachePage({super.key});
// ============================================================
// 图片预览
// ============================================================
void _previewImage(BuildContext context, CacheItem item) {
final file = File(item.path);
if (!file.existsSync()) return;
if (!CacheImageExtensions.isImage(item.path)) return;
showCupertinoDialog<void>(
context: context,
barrierColor: Colors.black87,
builder: (ctx) => GestureDetector(
onTap: () => Navigator.pop(ctx),
child: Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
Center(
child: PhotoView(
imageProvider: FileImage(file),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 3.0,
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
loadingBuilder: (_, __) =>
const Center(child: CupertinoActivityIndicator()),
),
),
Positioned(
top: MediaQuery.of(ctx).padding.top + 8,
right: 16,
child: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => Navigator.pop(ctx),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: CupertinoColors.black.withValues(alpha: 0.4),
borderRadius: AppRadius.fullBorder,
),
child: const Icon(
CupertinoIcons.xmark,
color: CupertinoColors.white,
size: 20,
),
),
),
),
Positioned(
bottom: MediaQuery.of(ctx).padding.bottom + 24,
left: 0,
right: 0,
child: Column(
children: [
Text(
CacheFormatter.formatSize(item.size),
style: AppTypography.subhead.copyWith(
color: CupertinoColors.white,
),
),
const SizedBox(height: 4),
Text(
CacheFormatter.formatDate(item.modified),
style: AppTypography.caption1.copyWith(
color: CupertinoColors.systemGrey5,
),
),
],
),
),
],
),
),
),
);
}
// ============================================================
// 删除确认
// ============================================================
void _confirmDeleteSingle(
BuildContext context,
WidgetRef ref,
CacheItem item,
) {
final ct = ProviderScope.containerOf(
context,
).read(translationsProvider).settings.cache;
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('🗑️ ${ct.cacheFiles}'),
content: Text(
'${ct.confirmDelete}${CacheFormatter.formatSize(item.size)}',
),
actions: [
CupertinoDialogAction(
child: Text(ct.cancel),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: Text(ct.delete),
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).deleteSingleItem(item);
AppToast.showSuccess(ct.deleted);
},
),
],
),
);
}
// ============================================================
// 清除过期缓存确认
// ============================================================
void _confirmClearExpired(
BuildContext context,
WidgetRef ref,
ImageCacheState state,
) {
final ct = ProviderScope.containerOf(
context,
).read(translationsProvider).settings.cache;
final expiredDays = state.expiredDays;
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('🗑️ ${ct.clearExpiredCache}'),
content: Text(
'${ct.confirmClearExpired}${expiredDays}${ct.beforeDays} ${state.expiredCount} ${ct.filesUnit} / ${CacheFormatter.formatSize(state.expiredSize)}',
),
actions: [
CupertinoDialogAction(
child: Text(ct.cancel),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: Text(ct.clear),
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).clearExpiredCache();
AppToast.showSuccess(ct.clearingExpired);
},
),
],
),
);
}
// ============================================================
// 清除全部缓存确认
// ============================================================
void _confirmClearAll(
BuildContext context,
WidgetRef ref,
ImageCacheState state,
) {
final ct = ProviderScope.containerOf(
context,
).read(translationsProvider).settings.cache;
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('⚠️ ${ct.clearAllCache}'),
content: Text(
'${ct.confirmClearAll}${state.cacheItems.length} ${ct.filesUnit} / ${CacheFormatter.formatSize(state.totalSize)}',
),
actions: [
CupertinoDialogAction(
child: Text(ct.cancel),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: Text(ct.clear),
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).clearAllCache();
AppToast.showSuccess(ct.clearingAll);
},
),
],
),
);
}
// ============================================================
// 批量删除确认
// ============================================================
void _confirmDeleteSelected(
BuildContext context,
WidgetRef ref,
ImageCacheState state,
) {
final ct = ProviderScope.containerOf(
context,
).read(translationsProvider).settings.cache;
final count = state.selectedPaths.length;
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('📋 ${ct.batchDelete}'),
content: Text(
'${ct.confirmBatchDelete}$count ${ct.filesUnit}${ct.irreversible}',
),
actions: [
CupertinoDialogAction(
child: Text(ct.cancel),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: Text(ct.delete),
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).deleteSelected();
AppToast.showSuccess(ct.clearingBatch);
},
),
],
),
);
}
// ============================================================
// 自动清理策略
// ============================================================
void _showAutoCleanPolicySheet(
BuildContext context,
WidgetRef ref,
ImageCacheState state,
) {
final ct = ProviderScope.containerOf(
context,
).read(translationsProvider).settings.cache;
final ext = AppTheme.ext(context);
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: Text('🔄 ${ct.autoCleanPolicy}'),
message: Text(ct.autoCleanPolicyDesc),
actions: AutoCleanPolicy.all.map((policy) {
return CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).setAutoCleanPolicy(policy);
AppToast.showSuccess(
'${ct.autoCleanPolicy}: ${AutoCleanPolicy.label(policy)}',
);
},
isDefaultAction: policy == state.autoCleanPolicy,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(AutoCleanPolicy.label(policy)),
if (policy == state.autoCleanPolicy) ...[
const SizedBox(width: 8),
Icon(
CupertinoIcons.checkmark_alt,
size: 18,
color: ext.accent,
),
],
],
),
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text(ct.cancel),
),
),
);
}
// ============================================================
// 缓存上限设置
// ============================================================
void _showCacheSizeLimitSheet(
BuildContext context,
WidgetRef ref,
ImageCacheState state,
) {
final ct = ProviderScope.containerOf(
context,
).read(translationsProvider).settings.cache;
final ext = AppTheme.ext(context);
const limits = [50, 100, 200, 500];
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: Text('📏 ${ct.cacheSizeLimit}'),
message: Text(ct.cacheSizeLimitDesc),
actions: limits.map((limit) {
return CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).setCacheSizeLimit(limit);
AppToast.showSuccess('${ct.cacheSizeLimit}: ${limit}MB');
},
isDefaultAction: limit == state.cacheSizeLimit,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${limit} MB'),
if (limit == state.cacheSizeLimit) ...[
const SizedBox(width: 8),
Icon(
CupertinoIcons.checkmark_alt,
size: 18,
color: ext.accent,
),
],
],
),
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text(ct.cancel),
),
),
);
}
// ============================================================
// 排序选择
// ============================================================
void _showSortSheet(
BuildContext context,
WidgetRef ref,
ImageCacheState state,
) {
final ct = ProviderScope.containerOf(
context,
).read(translationsProvider).settings.cache;
final ext = AppTheme.ext(context);
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: Text('📊 ${ct.sortBy}'),
actions: [
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).setSortMode(SortMode.date);
},
isDefaultAction: state.sortMode == SortMode.date,
child: _sortLabel(
ct.sortByDate,
SortMode.date,
state.sortMode,
ext,
),
),
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).setSortMode(SortMode.size);
},
isDefaultAction: state.sortMode == SortMode.size,
child: _sortLabel(
ct.sortBySize,
SortMode.size,
state.sortMode,
ext,
),
),
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
ref.read(imageCacheProvider.notifier).setSortMode(SortMode.type);
},
isDefaultAction: state.sortMode == SortMode.type,
child: _sortLabel(
ct.sortByType,
SortMode.type,
state.sortMode,
ext,
),
),
],
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text(ct.cancel),
),
),
);
}
Widget _sortLabel(
String label,
SortMode mode,
SortMode current,
AppThemeExtension ext,
) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label),
if (current == mode) ...[
const SizedBox(width: 8),
Icon(CupertinoIcons.checkmark_alt, size: 18, color: ext.accent),
],
],
);
}
// ============================================================
// 清理进度对话框
// ============================================================
void _showCleaningDialog(BuildContext context, ImageCacheState state) {
if (!state.isCleaning) return;
final ct = ProviderScope.containerOf(
context,
).read(translationsProvider).settings.cache;
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('🧹 ${ct.cleaningProgress}'),
content: Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
child: Column(
children: [
const CupertinoActivityIndicator(),
const SizedBox(height: AppSpacing.sm),
Text(
'${state.cleanProgress} / ${state.cleanTotal}',
style: AppTypography.footnote.copyWith(
color: AppTheme.ext(context).textSecondary,
),
),
const SizedBox(height: AppSpacing.sm),
ClipRRect(
borderRadius: AppRadius.xsBorder,
child: LinearProgressIndicator(
value: state.cleanTotal > 0
? state.cleanProgress / state.cleanTotal
: 0,
backgroundColor: AppTheme.ext(
context,
).textHint.withValues(alpha: 0.1),
valueColor: AlwaysStoppedAnimation<Color>(
AppTheme.ext(context).accent,
),
minHeight: 4,
),
),
],
),
),
),
);
}
// ============================================================
// 构建 UI
// ============================================================
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(imageCacheProvider);
final notifier = ref.read(imageCacheProvider.notifier);
final ext = AppTheme.ext(context);
final t = ref.watch(translationsProvider);
final ct = t.settings.cache;
if (state.isCleaning) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showCleaningDialog(context, state);
});
}
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
navigationBar: CupertinoNavigationBar(
leading: const AdaptiveBackButton(),
middle: Text(
'🖼️ ${ct.cacheManagement}',
style: AppTypography.title3.copyWith(color: ext.textPrimary),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!state.isBatchMode)
CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => _showSortSheet(context, ref, state),
child: Icon(
CupertinoIcons.sort_down,
color: ext.accent,
size: 22,
),
),
if (!state.isBatchMode)
CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => notifier.setViewMode(
state.viewMode == ViewMode.grid
? ViewMode.list
: ViewMode.grid,
),
child: Icon(
state.viewMode == ViewMode.grid
? CupertinoIcons.list_bullet
: CupertinoIcons.square_grid_2x2,
color: ext.accent,
size: 22,
),
),
CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => notifier.toggleBatchMode(),
child: Text(
state.isBatchMode ? ct.done : ct.edit,
style: AppTypography.footnote.copyWith(color: ext.accent),
),
),
],
),
backgroundColor: ext.bgElevated.withValues(alpha: 0.85),
border: null,
),
child: ResponsiveMaxWidth(
maxWidth: 900,
child: SafeArea(
bottom: false,
child: Stack(
children: [
if (state.isLoading && !state.isCleaning)
const Center(child: CupertinoActivityIndicator())
else if (state.error != null && state.cacheItems.isEmpty)
_buildErrorState(context, ref, state, ext, ct)
else
CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
CupertinoSliverRefreshControl(
onRefresh: () => notifier.loadCacheData(),
),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
sliver: SliverList(
delegate: SliverChildListDelegate([
StorageOverviewSection(
state: state,
ext: ext,
ct: ct,
),
const SizedBox(height: AppSpacing.md),
CategoryChipsSection(
state: state,
ext: ext,
ct: ct,
onCategorySelected: (cat) =>
notifier.setSelectedCategory(cat),
),
const SizedBox(height: AppSpacing.md),
CacheBreakdownSection(state: state, ext: ext, ct: ct),
const SizedBox(height: AppSpacing.md),
]),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
child: CacheGridSection(
state: state,
ext: ext,
ct: ct,
onPreviewImage: (item) => _previewImage(context, item),
onDeleteItem: (item) =>
_confirmDeleteSingle(context, ref, item),
onToggleSelect: (path) =>
notifier.toggleSelectPath(path),
onTapItem: (item) => Navigator.push<void>(
context,
CupertinoPageRoute<void>(
builder: (_) => ImageCacheDetailPage(item: item),
),
),
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
sliver: SliverList(
delegate: SliverChildListDelegate([
const SizedBox(height: AppSpacing.md),
CacheActionsSection(
state: state,
ext: ext,
ct: ct,
onClearExpired: () =>
_confirmClearExpired(context, ref, state),
onClearAll: () =>
_confirmClearAll(context, ref, state),
onShowAutoCleanPolicy: () =>
_showAutoCleanPolicySheet(context, ref, state),
onShowCacheSizeLimit: () =>
_showCacheSizeLimitSheet(context, ref, state),
onShowCleanLog: () => Navigator.push<void>(
context,
CupertinoPageRoute<void>(
builder: (_) => const ImageCacheLogPage(),
),
),
),
const SizedBox(height: AppSpacing.xxl),
]),
),
),
],
),
if (state.isBatchMode)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: BatchActionBar(
state: state,
ext: ext,
ct: ct,
onSelectAll: () => notifier.selectAll(),
onDeselectAll: () => notifier.deselectAll(),
onDeleteSelected: () =>
_confirmDeleteSelected(context, ref, state),
onCancel: () => notifier.toggleBatchMode(),
),
),
],
),
),
),
);
}
// ============================================================
// 错误状态UI
// ============================================================
Widget _buildErrorState(
BuildContext context,
WidgetRef ref,
ImageCacheState state,
AppThemeExtension ext,
TSettingsCache ct,
) {
return Center(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('⚠️', style: TextStyle(fontSize: 48)),
const SizedBox(height: AppSpacing.sm),
Text(
state.error ?? ct.loadFailed,
style: AppTypography.subhead.copyWith(color: ext.textHint),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
onPressed: () =>
ref.read(imageCacheProvider.notifier).loadCacheData(),
child: Text(
ct.retry,
style: AppTypography.subhead.copyWith(color: ext.accent),
),
),
],
),
),
);
}
}