- 引导页协议多语言支持(languageId传递) - 登录页双书名号修复 + 注册页协议勾选 - 个人中心页面多语言(18个翻译键) - 网络断开提示增加关闭/刷新按钮 - 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索 - iOS快捷按钮重复修复(删除Info.plist静态定义) - 测试账号123456警告提示 - 扫码登录自动跳转(HTTP轮询+WebSocket双通道) - 登录页老用户按钮改次要色 - Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0) - macOS标题栏跟随软件夜间模式 - 平台兼容分发渠道弹窗 - 软件著作权图片+交叉水印 - 桌面小部件平台兼容说明默认收起 - iOS/macOS图标更新+名称确认为闲言 - 12个语言文件补全roleNative+7个分发渠道翻译字段
539 lines
19 KiB
Dart
539 lines
19 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/deferred_builder.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: DeferredBuilder(builder: (context) => 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;
|
|
}
|