Files
xianyan/lib/features/mine/settings/presentation/data_management_page.dart
Developer 182735df3b release: 发布v6.6.2版本,优化多项功能与体验
本次更新包含:
1.  更新应用标语与隐私政策文案,调整品牌宣传语
2.  重构Feed ID解析、HTML清理工具类,提取重复逻辑
3.  新增全屏图片查看器、通用动画操作按钮组件
4.  修复电池监听空指针、快捷操作异常捕获问题
5.  优化搜索、会话列表、RSS阅读器等页面体验
6.  完善多语言支持,新增多个翻译模块
7.  移除冗余代码,统一数字格式化逻辑
8.  调整登录页面布局与交互逻辑
2026-06-01 08:16:01 +08:00

538 lines
18 KiB
Dart

/// ============================================================
/// 闲言APP — 数据管理页面
/// 创建时间: 2026-04-28
/// 更新时间: 2026-06-01
/// 作用: 真实存储统计+分类详情+分类清理+空间可视化+完整导出导入+自动备份
/// 上次更新: 替换所有硬编码中文为多语言调用
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import '../../../../core/services/data/backup_service.dart';
import '../../../../core/storage/database/app_database.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../core/theme/app_radius.dart';
import '../../../../core/router/app_nav_extension.dart';
import '../../../../core/router/app_routes.dart';
import '../../../../l10n/translations.dart';
import '../../../../shared/widgets/feedback/app_toast.dart';
import '../../../../shared/widgets/containers/glass_container.dart';
import '../../../../shared/widgets/adaptive/responsive_layout.dart';
import '../providers/general_settings_provider.dart';
import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart';
import 'data_management_state_base.dart';
import 'data_management_backup_mixin.dart';
import 'data_management_export_mixin.dart';
import 'data_management_widgets.dart';
class DataManagementPage extends ConsumerStatefulWidget {
const DataManagementPage({super.key});
@override
ConsumerState<DataManagementPage> createState() => _DataManagementPageState();
}
class _DataManagementPageState extends ConsumerState<DataManagementPage>
with
DataManagementStateBase<DataManagementPage>,
DataManagementBackupMixin<DataManagementPage>,
DataManagementExportMixin<DataManagementPage> {
T get _t => ref.read(translationsProvider);
@override
void initState() {
super.initState();
loadAllStats();
}
@override
Future<void> loadAllStats() async {
final db = AppDatabase.instance;
final settings = ref.read(generalSettingsProvider);
final results = await Future.wait([
db.getFavoriteCount(),
db.getReadHistoryCount(),
db.getNoteCount(),
db.getShareHistoryCount(),
db.getFeedCacheSize(),
db.getHanziCacheCount(),
db.getOfflineActionCount(),
]);
autoBackupEnabled = BackupService.autoBackupEnabled;
backups = await BackupService.getBackupList();
totalBackupSize = await BackupService.getTotalBackupSize();
if (mounted) {
setState(() {
favCount = results[0];
historyCount = results[1];
noteCount = results[2];
shareCount = results[3];
feedCacheCount = results[4];
hanziCacheCount = results[5];
offlineCount = results[6];
cacheSizeText = settings.cacheSizeText;
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final t = ref.watch(translationsProvider);
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
navigationBar: CupertinoNavigationBar(
leading: const AdaptiveBackButton(),
middle: Text(
'💾 ${t.dataManagement.title}',
style: AppTypography.title3.copyWith(color: ext.textPrimary),
),
backgroundColor: ext.bgElevated.withValues(alpha: 0.85),
border: null,
),
child: ResponsiveMaxWidth(
maxWidth: 900,
child: SafeArea(
bottom: false,
child: isLoading
? const Center(child: CupertinoActivityIndicator())
: ListView(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
children: [
_buildStorageOverview(ext),
const SizedBox(height: AppSpacing.md),
_buildStorageChart(ext),
const SizedBox(height: AppSpacing.md),
buildAutoBackup(ext),
const SizedBox(height: AppSpacing.md),
_buildDataCategories(ext),
const SizedBox(height: AppSpacing.md),
buildExportImport(ext),
const SizedBox(height: AppSpacing.md),
_buildDangerZone(ext),
const SizedBox(height: AppSpacing.xxl),
],
),
),
),
);
}
Widget _buildStorageOverview(AppThemeExtension ext) {
final t = _t;
return GlassContainer(
depth: GlassDepth.elevated,
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(CupertinoIcons.chart_pie_fill, size: 18, color: ext.accent),
const SizedBox(width: AppSpacing.sm),
Text(
t.dataManagement.storageOverview,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
t.dataManagement.totalLocalData.replaceAll('{count}', '$totalLocalItems'),
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
const SizedBox(height: AppSpacing.md),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
DataOverviewItem(
ext: ext,
icon: CupertinoIcons.heart_fill,
label: t.dataManagement.favorites,
value: '$favCount ${t.dataManagement.entriesUnit}',
color: CupertinoColors.systemPink,
),
DataOverviewItem(
ext: ext,
icon: CupertinoIcons.book_fill,
label: t.dataManagement.history,
value: '$historyCount ${t.dataManagement.entriesUnit}',
color: ext.accent,
),
DataOverviewItem(
ext: ext,
icon: CupertinoIcons.pencil_ellipsis_rectangle,
label: t.dataManagement.notes,
value: '$noteCount ${t.dataManagement.entriesUnit}',
color: CupertinoColors.systemGreen,
),
DataOverviewItem(
ext: ext,
icon: CupertinoIcons.share,
label: t.dataManagement.shares,
value: '$shareCount ${t.dataManagement.entriesUnit}',
color: CupertinoColors.systemOrange,
),
],
),
],
),
).animate().fadeIn(duration: 300.ms);
}
Widget _buildStorageChart(AppThemeExtension ext) {
if (totalLocalItems == 0) return const SizedBox.shrink();
final t = _t;
final data = <(String, int, Color)>[
(t.dataManagement.favorites, favCount, const Color(0xFFFF2D55)),
(t.dataManagement.history, historyCount, ext.accent),
(t.dataManagement.notes, noteCount, const Color(0xFF34C759)),
(t.dataManagement.shares, shareCount, const Color(0xFFFF9500)),
];
final pieData = data
.where((d) => d.$2 > 0)
.map((d) => _StoragePie(d.$1, d.$2.toDouble(), d.$3))
.toList();
return GlassContainer(
depth: GlassDepth.elevated,
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(CupertinoIcons.chart_pie_fill, size: 18, color: ext.accent),
const SizedBox(width: AppSpacing.sm),
Text(
t.dataManagement.dataDistribution,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppSpacing.md),
SizedBox(
height: 160,
child: SfCircularChart(
margin: EdgeInsets.zero,
series: [
DoughnutSeries<_StoragePie, String>(
animationDuration: 0,
dataSource: pieData,
xValueMapper: (d, _) => d.label,
yValueMapper: (d, _) => d.value,
pointColorMapper: (d, _) => d.color,
innerRadius: '35%',
radius: '70%',
dataLabelSettings: DataLabelSettings(
isVisible: true,
textStyle: AppTypography.caption2.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
fontSize: 9,
),
),
),
],
),
),
const SizedBox(height: AppSpacing.sm),
Wrap(
spacing: AppSpacing.md,
runSpacing: AppSpacing.xs,
children: data
.where((d) => d.$2 > 0)
.map(
(d) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: d.$3,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
'${d.$1} ${d.$2}',
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
],
),
)
.toList(),
),
],
),
).animate().fadeIn(duration: 300.ms, delay: 100.ms);
}
Widget _buildDataCategories(AppThemeExtension ext) {
final t = _t;
final categories = <DataCategory>[
DataCategory(
icon: CupertinoIcons.heart_fill,
label: t.dataManagement.favoriteData,
count: favCount,
description: t.dataManagement.localFavorites.replaceAll('{count}', '$favCount'),
onClear: () => _clearCategory(ext, t.dataManagement.favorites, () async {
await AppDatabase.instance.clearAllFavorites();
await loadAllStats();
}),
),
DataCategory(
icon: CupertinoIcons.book_fill,
label: t.dataManagement.readingHistory,
count: historyCount,
description: t.dataManagement.localHistory.replaceAll('{count}', '$historyCount'),
onClear: () => _clearCategory(ext, t.dataManagement.readingHistory, () async {
await AppDatabase.instance.clearAllReadHistory();
await loadAllStats();
}),
),
DataCategory(
icon: CupertinoIcons.pencil_ellipsis_rectangle,
label: t.dataManagement.noteData,
count: noteCount,
description: t.dataManagement.localNotes.replaceAll('{count}', '$noteCount'),
onClear: () => _clearCategory(ext, t.dataManagement.notes, () async {
await AppDatabase.instance.clearAllNotes();
await loadAllStats();
}),
),
DataCategory(
icon: CupertinoIcons.share,
label: t.dataManagement.shareHistory,
count: shareCount,
description: t.dataManagement.shareRecords.replaceAll('{count}', '$shareCount'),
onClear: () => _clearCategory(ext, t.dataManagement.shareHistory, () async {
await AppDatabase.instance.clearAllShareHistory();
await loadAllStats();
}),
),
DataCategory(
icon: CupertinoIcons.photo_fill,
label: t.dataManagement.imageCache,
count: feedCacheCount,
description: cacheSizeText,
onTap: () => context.appPush(AppRoutes.imageCache),
onClear: () => _clearCategory(ext, t.dataManagement.imageCache, () async {
ref.read(generalSettingsProvider.notifier).clearCache();
await AppDatabase.instance.clearFeedCache();
await loadAllStats();
}),
),
DataCategory(
icon: CupertinoIcons.textformat_abc,
label: t.dataManagement.hanziCache,
count: hanziCacheCount,
description: t.dataManagement.queryCache.replaceAll('{count}', '$hanziCacheCount'),
onClear: () => _clearCategory(ext, t.dataManagement.hanziCache, () async {
await AppDatabase.instance.clearAllHanziCache();
await loadAllStats();
}),
),
DataCategory(
icon: CupertinoIcons.arrow_2_circlepath,
label: t.dataManagement.offlineQueue,
count: offlineCount,
description: t.dataManagement.pendingSync.replaceAll('{count}', '$offlineCount'),
onClear: () => _clearCategory(ext, t.dataManagement.offlineQueue, () async {
await AppDatabase.instance.clearOfflineQueue();
await loadAllStats();
}),
),
];
return GlassContainer(
depth: GlassDepth.elevated,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md,
AppSpacing.md,
AppSpacing.md,
AppSpacing.sm,
),
child: Row(
children: [
Icon(CupertinoIcons.folder_fill, size: 18, color: ext.accent),
const SizedBox(width: AppSpacing.sm),
Text(
t.dataManagement.dataCategories,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
),
...categories.map(
(cat) => Column(
children: [
DataCategoryRow(ext: ext, category: cat),
if (cat != categories.last)
Divider(
height: 0.5,
thickness: 0.5,
indent: AppSpacing.md + 28 + AppSpacing.sm,
color: ext.textHint.withValues(alpha: 0.15),
),
],
),
),
],
),
).animate().fadeIn(duration: 300.ms, delay: 150.ms);
}
void _clearCategory(
AppThemeExtension ext,
String name,
Future<void> Function() onClear,
) {
final t = _t;
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('🗑️ ${t.dataManagement.clearName.replaceAll('{name}', name)}'),
content: Text(t.dataManagement.clearConfirm.replaceAll('{name}', name)),
actions: [
CupertinoDialogAction(
child: Text(t.common.cancel),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: Text(t.common.clear),
onPressed: () async {
Navigator.pop(ctx);
await onClear();
if (mounted) AppToast.showSuccess(t.dataManagement.cleared.replaceAll('{name}', name));
},
),
],
),
);
}
Widget _buildDangerZone(AppThemeExtension ext) {
final t = _t;
return GlassContainer(
depth: GlassDepth.elevated,
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
CupertinoIcons.exclamationmark_triangle_fill,
size: 18,
color: CupertinoColors.systemRed,
),
const SizedBox(width: AppSpacing.sm),
Text(
t.dataManagement.dangerZone,
style: AppTypography.subhead.copyWith(
color: CupertinoColors.systemRed,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppSpacing.sm),
SizedBox(
width: double.infinity,
child: CupertinoButton(
color: CupertinoColors.systemRed.withValues(alpha: 0.1),
borderRadius: AppRadius.mdBorder,
onPressed: () {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('⚠️ ${t.dataManagement.clearAllData}'),
content: Text(t.dataManagement.clearAllConfirm),
actions: [
CupertinoDialogAction(
child: Text(t.common.cancel),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: Text(t.dataManagement.clearAll),
onPressed: () async {
Navigator.pop(ctx);
await _clearAllData();
},
),
],
),
);
},
child: Text(
'🗑️ ${t.dataManagement.clearAllLocalData}',
style: AppTypography.subhead.copyWith(
color: CupertinoColors.systemRed,
),
),
),
),
],
),
);
}
Future<void> _clearAllData() async {
final db = AppDatabase.instance;
await db.clearAllFavorites();
await db.clearAllReadHistory();
await db.clearAllNotes();
await db.clearAllShareHistory();
await db.clearFeedCache();
await db.clearAllHanziCache();
await db.clearOfflineQueue();
ref.read(generalSettingsProvider.notifier).clearCache();
await loadAllStats();
if (mounted) AppToast.showSuccess(_t.dataManagement.allDataCleared);
}
}
class _StoragePie {
const _StoragePie(this.label, this.value, this.color);
final String label;
final double value;
final Color color;
}