本次提交包含多项迭代优化和问题修复: 1. 新增缩略图图片组件、数字格式化工具类,补充多语言翻译类型与本地化支持 2. 优化底部导航栏主题色统一使用动态accent色值 3. 修复多处图表动画、路由跳转、API请求相关问题 4. 简化服务器公告文案,调整默认分屏状态为关闭 5. 新增安卓/iOS桌面快捷方式配置 6. 重构多处状态管理类使用SafeNotifierInit统一异常保护 7. 替换硬编码蓝色为主题色,更新版本号获取方式为动态读取 8. 优化缓存预加载逻辑,移除无用代码 9. 调整默认设置项,优化用户体验细节
841 lines
26 KiB
Dart
841 lines
26 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 图片缓存概览/筛选/占比/操作区组件
|
||
/// 创建时间: 2026-05-30
|
||
/// 更新时间: 2026-05-31
|
||
/// 作用: 图片缓存页面的存储概览、分类筛选、缓存占比(饼图)、操作区、批量操作栏
|
||
/// 上次更新: 修复缓存为空时显示0B问题,添加空态UI
|
||
/// ============================================================
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_animate/flutter_animate.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/containers/glass_container.dart';
|
||
import 'image_cache_models.dart';
|
||
import 'image_cache_pie_chart.dart';
|
||
|
||
// ============================================================
|
||
// 存储概览区
|
||
// ============================================================
|
||
|
||
/// 存储概览区:总缓存/文件数/Feed缓存 + 进度条(动态上限)
|
||
class StorageOverviewSection extends StatelessWidget {
|
||
StorageOverviewSection({
|
||
required this.state,
|
||
required this.ext,
|
||
required this.ct,
|
||
super.key,
|
||
});
|
||
|
||
final ImageCacheState state;
|
||
final AppThemeExtension ext;
|
||
final TSettingsCache ct;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final hasCache = state.cacheItems.isNotEmpty;
|
||
|
||
if (!hasCache) {
|
||
return GlassContainer(
|
||
depth: GlassDepth.elevated,
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
child: Column(
|
||
children: [
|
||
_SectionHeader(
|
||
icon: CupertinoIcons.photo_fill,
|
||
title: ct.storageOverview,
|
||
ext: ext,
|
||
),
|
||
const SizedBox(height: AppSpacing.lg),
|
||
Icon(
|
||
CupertinoIcons.tray,
|
||
size: 48,
|
||
color: ext.textHint.withValues(alpha: 0.4),
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
Text(
|
||
ct.totalCache.isEmpty ? '暂无缓存' : ct.totalCache,
|
||
style: AppTypography.subhead.copyWith(color: ext.textHint),
|
||
),
|
||
const SizedBox(height: AppSpacing.xs),
|
||
Text(
|
||
'浏览图片后缓存将自动出现在这里',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textHint.withValues(alpha: 0.6),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
).animate().fadeIn(duration: 300.ms);
|
||
}
|
||
|
||
final limitBytes = state.cacheSizeLimit * 1024 * 1024;
|
||
final progress = state.totalSize > 0
|
||
? (state.totalSize / limitBytes).clamp(0.0, 1.0)
|
||
: 0.0;
|
||
|
||
return GlassContainer(
|
||
depth: GlassDepth.elevated,
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_SectionHeader(
|
||
icon: CupertinoIcons.photo_fill,
|
||
title: ct.storageOverview,
|
||
ext: ext,
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: [
|
||
_StatItem(
|
||
ext: ext,
|
||
icon: CupertinoIcons.folder_fill,
|
||
label: ct.totalCache,
|
||
value: CacheFormatter.formatSize(state.totalSize),
|
||
color: CupertinoColors.systemBlue,
|
||
),
|
||
_StatItem(
|
||
ext: ext,
|
||
icon: CupertinoIcons.photo_fill,
|
||
label: ct.fileCount,
|
||
value: '${state.cacheItems.length}',
|
||
color: CupertinoColors.systemGreen,
|
||
),
|
||
_StatItem(
|
||
ext: ext,
|
||
icon: CupertinoIcons.doc_text_fill,
|
||
label: ct.feedCache,
|
||
value: '${state.imageCacheCount} ${ct.filesUnit}',
|
||
color: CupertinoColors.systemOrange,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
ClipRRect(
|
||
borderRadius: AppRadius.smBorder,
|
||
child: LinearProgressIndicator(
|
||
value: progress,
|
||
backgroundColor: ext.textHint.withValues(alpha: 0.1),
|
||
valueColor: AlwaysStoppedAnimation<Color>(ext.accent),
|
||
minHeight: 6,
|
||
),
|
||
),
|
||
const SizedBox(height: AppSpacing.xs),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'${ct.usedSpace} ${CacheFormatter.formatSize(state.totalSize)}',
|
||
style: AppTypography.caption1.copyWith(color: ext.textHint),
|
||
),
|
||
Text(
|
||
'${ct.refLimit} ${state.cacheSizeLimit} MB',
|
||
style: AppTypography.caption1.copyWith(color: ext.textHint),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
).animate().fadeIn(duration: 300.ms);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 分类筛选 Chips
|
||
// ============================================================
|
||
|
||
/// 分类筛选横条
|
||
class CategoryChipsSection extends StatelessWidget {
|
||
CategoryChipsSection({
|
||
required this.state,
|
||
required this.ext,
|
||
required this.ct,
|
||
required this.onCategorySelected,
|
||
super.key,
|
||
});
|
||
|
||
final ImageCacheState state;
|
||
final AppThemeExtension ext;
|
||
final TSettingsCache ct;
|
||
final ValueChanged<String?> onCategorySelected;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return SizedBox(
|
||
height: 36,
|
||
child: ListView(
|
||
scrollDirection: Axis.horizontal,
|
||
children: [
|
||
_CategoryChip(
|
||
ext: ext,
|
||
label: '📋 ${ct.all}',
|
||
category: null,
|
||
selectedCategory: state.selectedCategory,
|
||
onTap: onCategorySelected,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
...CacheCategory.all.map((cat) {
|
||
final count = state.categoryCounts[cat] ?? 0;
|
||
return Padding(
|
||
padding: const EdgeInsets.only(right: AppSpacing.sm),
|
||
child: _CategoryChip(
|
||
ext: ext,
|
||
label: '${CacheCategory.label(cat)} ($count)',
|
||
category: cat,
|
||
selectedCategory: state.selectedCategory,
|
||
onTap: onCategorySelected,
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 缓存占比区(饼图)
|
||
// ============================================================
|
||
|
||
/// 缓存占比区:饼图 + 近期/过期 + 分类详情行
|
||
class CacheBreakdownSection extends StatelessWidget {
|
||
CacheBreakdownSection({
|
||
required this.state,
|
||
required this.ext,
|
||
required this.ct,
|
||
super.key,
|
||
});
|
||
|
||
final ImageCacheState state;
|
||
final AppThemeExtension ext;
|
||
final TSettingsCache ct;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final recentSize = state.totalSize - state.expiredSize;
|
||
final recentCount = state.cacheItems.length - state.expiredCount;
|
||
final expiredDays = state.expiredDays;
|
||
|
||
final segments = <PieSegment>[
|
||
PieSegment(
|
||
label: '${ct.recentCache}(${expiredDays}${ct.withinDays})',
|
||
size: recentSize,
|
||
color: CupertinoColors.systemBlue,
|
||
),
|
||
PieSegment(
|
||
label: '${ct.expiredCache}(${expiredDays}${ct.beforeDays})',
|
||
size: state.expiredSize,
|
||
color: CupertinoColors.systemOrange,
|
||
),
|
||
...CacheCategory.all.map((cat) {
|
||
final size = state.categoryStats[cat] ?? 0;
|
||
if (size <= 0) return null;
|
||
return PieSegment(
|
||
label: CacheCategory.label(cat),
|
||
size: size,
|
||
color: CacheCategoryStyle.color(cat),
|
||
);
|
||
}).whereType<PieSegment>(),
|
||
];
|
||
|
||
return GlassContainer(
|
||
depth: GlassDepth.elevated,
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_SectionHeader(
|
||
icon: CupertinoIcons.chart_pie_fill,
|
||
title: ct.cacheBreakdown,
|
||
ext: ext,
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
CachePieChart(
|
||
segments: segments,
|
||
totalSize: state.totalSize,
|
||
ext: ext,
|
||
ct: ct,
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_BreakdownRow(
|
||
ext: ext,
|
||
icon: CupertinoIcons.clock_fill,
|
||
label: '${ct.recentCache}(${expiredDays}${ct.withinDays})',
|
||
count: recentCount,
|
||
size: recentSize,
|
||
ratio: CacheFormatter.cacheRatio(recentSize, state.totalSize),
|
||
color: CupertinoColors.systemBlue,
|
||
ct: ct,
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
_BreakdownRow(
|
||
ext: ext,
|
||
icon: CupertinoIcons.clock_fill,
|
||
label: '${ct.expiredCache}(${expiredDays}${ct.beforeDays})',
|
||
count: state.expiredCount,
|
||
size: state.expiredSize,
|
||
ratio: CacheFormatter.cacheRatio(
|
||
state.expiredSize,
|
||
state.totalSize,
|
||
),
|
||
color: CupertinoColors.systemOrange,
|
||
ct: ct,
|
||
),
|
||
if (state.categoryStats.isNotEmpty) ...[
|
||
const SizedBox(height: AppSpacing.md),
|
||
Divider(color: ext.textHint.withValues(alpha: 0.1)),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
...CacheCategory.all.map((cat) {
|
||
final size = state.categoryStats[cat] ?? 0;
|
||
final count = state.categoryCounts[cat] ?? 0;
|
||
if (count == 0) return const SizedBox.shrink();
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
|
||
child: _BreakdownRow(
|
||
ext: ext,
|
||
icon: CacheCategoryStyle.icon(cat),
|
||
label: CacheCategory.label(cat),
|
||
count: count,
|
||
size: size,
|
||
ratio: CacheFormatter.cacheRatio(size, state.totalSize),
|
||
color: CacheCategoryStyle.color(cat),
|
||
ct: ct,
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
],
|
||
),
|
||
).animate().fadeIn(duration: 300.ms, delay: 100.ms);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 操作区
|
||
// ============================================================
|
||
|
||
/// 缓存操作区:自动清理策略 + 缓存上限 + 清理日志 + 清除过期 + 清除全部
|
||
class CacheActionsSection extends StatelessWidget {
|
||
CacheActionsSection({
|
||
required this.state,
|
||
required this.ext,
|
||
required this.ct,
|
||
required this.onClearExpired,
|
||
required this.onClearAll,
|
||
required this.onShowAutoCleanPolicy,
|
||
required this.onShowCacheSizeLimit,
|
||
required this.onShowCleanLog,
|
||
super.key,
|
||
});
|
||
|
||
final ImageCacheState state;
|
||
final AppThemeExtension ext;
|
||
final TSettingsCache ct;
|
||
final VoidCallback onClearExpired;
|
||
final VoidCallback onClearAll;
|
||
final VoidCallback onShowAutoCleanPolicy;
|
||
final VoidCallback onShowCacheSizeLimit;
|
||
final VoidCallback onShowCleanLog;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final expiredDays = state.expiredDays;
|
||
|
||
return GlassContainer(
|
||
depth: GlassDepth.elevated,
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_SectionHeader(
|
||
icon: CupertinoIcons.trash_fill,
|
||
title: ct.cacheActions,
|
||
ext: ext,
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_ActionRow(
|
||
ext: ext,
|
||
icon: CupertinoIcons.arrow_counterclockwise,
|
||
iconColor: ext.accent,
|
||
title: ct.autoCleanPolicy,
|
||
subtitle:
|
||
'${ct.currentLabel}: ${AutoCleanPolicy.label(state.autoCleanPolicy)}${ct.autoCleanSuffix}',
|
||
onTap: onShowAutoCleanPolicy,
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
_ActionRow(
|
||
ext: ext,
|
||
icon: CupertinoIcons.gauge,
|
||
iconColor: ext.accent,
|
||
title: ct.cacheSizeLimit,
|
||
subtitle: '${ct.currentLimitLabel}: ${state.cacheSizeLimit} MB',
|
||
onTap: onShowCacheSizeLimit,
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
_ActionRow(
|
||
ext: ext,
|
||
icon: CupertinoIcons.doc_text_search,
|
||
iconColor: ext.accent,
|
||
title: ct.cleanLog,
|
||
subtitle: ct.cleanLogDesc,
|
||
onTap: onShowCleanLog,
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: CupertinoButton(
|
||
color: CupertinoColors.systemOrange.withValues(alpha: 0.1),
|
||
borderRadius: AppRadius.mdBorder,
|
||
onPressed: state.expiredCount > 0 ? onClearExpired : null,
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(
|
||
CupertinoIcons.clock_fill,
|
||
size: 18,
|
||
color: CupertinoColors.systemOrange,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Text(
|
||
'${ct.clearExpiredCache}(${expiredDays}${ct.beforeDays} · ${state.expiredCount}${ct.filesUnit} / ${CacheFormatter.formatSize(state.expiredSize)})',
|
||
style: AppTypography.subhead.copyWith(
|
||
color: state.expiredCount > 0
|
||
? CupertinoColors.systemOrange
|
||
: CupertinoColors.systemGrey,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: CupertinoButton(
|
||
color: CupertinoColors.systemRed.withValues(alpha: 0.1),
|
||
borderRadius: AppRadius.mdBorder,
|
||
onPressed: state.cacheItems.isNotEmpty ? onClearAll : null,
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(
|
||
CupertinoIcons.trash_fill,
|
||
size: 18,
|
||
color: CupertinoColors.systemRed,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Text(
|
||
'${ct.clearAllCache}(${state.cacheItems.length}${ct.filesUnit} / ${CacheFormatter.formatSize(state.totalSize)})',
|
||
style: AppTypography.subhead.copyWith(
|
||
color: state.cacheItems.isNotEmpty
|
||
? CupertinoColors.systemRed
|
||
: CupertinoColors.systemGrey,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
).animate().fadeIn(duration: 300.ms, delay: 300.ms);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 批量操作底部栏
|
||
// ============================================================
|
||
|
||
/// 批量操作底部栏:选中数量 + 全选/取消全选/删除选中/取消
|
||
class BatchActionBar extends StatelessWidget {
|
||
BatchActionBar({
|
||
required this.state,
|
||
required this.ext,
|
||
required this.ct,
|
||
required this.onSelectAll,
|
||
required this.onDeselectAll,
|
||
required this.onDeleteSelected,
|
||
required this.onCancel,
|
||
super.key,
|
||
});
|
||
|
||
final ImageCacheState state;
|
||
final AppThemeExtension ext;
|
||
final TSettingsCache ct;
|
||
final VoidCallback onSelectAll;
|
||
final VoidCallback onDeselectAll;
|
||
final VoidCallback onDeleteSelected;
|
||
final VoidCallback onCancel;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final selectedCount = state.selectedPaths.length;
|
||
final totalCount = state.cacheItems.length;
|
||
final allSelected = selectedCount == totalCount && totalCount > 0;
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.sm,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgCard.withValues(alpha: 0.95),
|
||
border: Border(
|
||
top: BorderSide(
|
||
color: ext.textHint.withValues(alpha: 0.15),
|
||
width: 0.5,
|
||
),
|
||
),
|
||
),
|
||
child: SafeArea(
|
||
top: false,
|
||
child: Row(
|
||
children: [
|
||
Text(
|
||
'${ct.selectedCount} $selectedCount',
|
||
style: AppTypography.subhead.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
_BatchButton(
|
||
ext: ext,
|
||
label: allSelected ? ct.deselectAll : ct.selectAll,
|
||
icon: allSelected
|
||
? CupertinoIcons.checkmark_rectangle
|
||
: CupertinoIcons.square_grid_2x2,
|
||
color: ext.accent,
|
||
onTap: allSelected ? onDeselectAll : onSelectAll,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
_BatchButton(
|
||
ext: ext,
|
||
label: ct.delete,
|
||
icon: CupertinoIcons.trash_fill,
|
||
color: CupertinoColors.systemRed,
|
||
onTap: selectedCount > 0 ? onDeleteSelected : null,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
_BatchButton(
|
||
ext: ext,
|
||
label: ct.cancel,
|
||
icon: CupertinoIcons.xmark,
|
||
color: ext.textSecondary,
|
||
onTap: onCancel,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
).animate().slideY(begin: 1, end: 0, duration: 250.ms);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 私有组件
|
||
// ============================================================
|
||
|
||
/// 区块标题行
|
||
class _SectionHeader extends StatelessWidget {
|
||
const _SectionHeader({
|
||
required this.icon,
|
||
required this.title,
|
||
required this.ext,
|
||
});
|
||
|
||
final IconData icon;
|
||
final String title;
|
||
final AppThemeExtension ext;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Icon(icon, size: 18, color: ext.accent),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Text(
|
||
title,
|
||
style: AppTypography.subhead.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 统计项小组件
|
||
class _StatItem extends StatelessWidget {
|
||
const _StatItem({
|
||
required this.ext,
|
||
required this.icon,
|
||
required this.label,
|
||
required this.value,
|
||
required this.color,
|
||
});
|
||
|
||
final AppThemeExtension ext;
|
||
final IconData icon;
|
||
final String label;
|
||
final String value;
|
||
final Color color;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
Icon(icon, size: 20, color: color),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
value,
|
||
style: AppTypography.subhead.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
Text(
|
||
label,
|
||
style: AppTypography.caption2.copyWith(color: ext.textHint),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 单个分类筛选 Chip
|
||
class _CategoryChip extends StatelessWidget {
|
||
const _CategoryChip({
|
||
required this.ext,
|
||
required this.label,
|
||
required this.category,
|
||
required this.selectedCategory,
|
||
required this.onTap,
|
||
});
|
||
|
||
final AppThemeExtension ext;
|
||
final String label;
|
||
final String? category;
|
||
final String? selectedCategory;
|
||
final ValueChanged<String?> onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final selected = selectedCategory == category;
|
||
return GestureDetector(
|
||
onTap: () => onTap(category),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: selected
|
||
? ext.accent.withValues(alpha: 0.15)
|
||
: ext.bgSecondary,
|
||
borderRadius: AppRadius.pillBorder,
|
||
border: Border.all(
|
||
color: selected
|
||
? ext.accent.withValues(alpha: 0.5)
|
||
: ext.textHint.withValues(alpha: 0.15),
|
||
),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: selected ? ext.accent : ext.textSecondary,
|
||
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 占比行组件
|
||
class _BreakdownRow extends StatelessWidget {
|
||
_BreakdownRow({
|
||
required this.ext,
|
||
required this.icon,
|
||
required this.label,
|
||
required this.count,
|
||
required this.size,
|
||
required this.ratio,
|
||
required this.color,
|
||
required this.ct,
|
||
});
|
||
|
||
final AppThemeExtension ext;
|
||
final IconData icon;
|
||
final String label;
|
||
final int count;
|
||
final int size;
|
||
final double ratio;
|
||
final Color color;
|
||
final TSettingsCache ct;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Icon(icon, size: 16, color: color),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: AppTypography.footnote.copyWith(color: ext.textPrimary),
|
||
),
|
||
const SizedBox(height: 4),
|
||
ClipRRect(
|
||
borderRadius: AppRadius.xsBorder,
|
||
child: LinearProgressIndicator(
|
||
value: ratio.clamp(0.0, 1.0),
|
||
backgroundColor: ext.textHint.withValues(alpha: 0.1),
|
||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||
minHeight: 4,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Text(
|
||
CacheFormatter.formatSize(size),
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
Text(
|
||
'$count ${ct.filesUnit}',
|
||
style: AppTypography.caption2.copyWith(color: ext.textHint),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 操作行组件(自动清理策略 / 缓存上限 / 清理日志 通用行)
|
||
class _ActionRow extends StatelessWidget {
|
||
const _ActionRow({
|
||
required this.ext,
|
||
required this.icon,
|
||
required this.iconColor,
|
||
required this.title,
|
||
required this.subtitle,
|
||
required this.onTap,
|
||
});
|
||
|
||
final AppThemeExtension ext;
|
||
final IconData icon;
|
||
final Color iconColor;
|
||
final String title;
|
||
final String subtitle;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.sm,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgSecondary,
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(icon, size: 18, color: iconColor),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: AppTypography.footnote.copyWith(
|
||
color: ext.textPrimary,
|
||
),
|
||
),
|
||
Text(
|
||
subtitle,
|
||
style: AppTypography.caption2.copyWith(color: ext.textHint),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Icon(CupertinoIcons.chevron_right, size: 16, color: ext.textHint),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 批量操作按钮
|
||
class _BatchButton extends StatelessWidget {
|
||
const _BatchButton({
|
||
required this.ext,
|
||
required this.label,
|
||
required this.icon,
|
||
required this.color,
|
||
required this.onTap,
|
||
});
|
||
|
||
final AppThemeExtension ext;
|
||
final String label;
|
||
final IconData icon;
|
||
final Color color;
|
||
final VoidCallback? onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final enabled = onTap != null;
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: 6,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: enabled
|
||
? color.withValues(alpha: 0.12)
|
||
: ext.textHint.withValues(alpha: 0.06),
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, size: 16, color: enabled ? color : ext.textHint),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
label,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: enabled ? color : ext.textHint,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|