Files
xianyan/lib/features/mine/settings/presentation/other_settings_page.dart
2026-06-06 06:54:22 +08:00

828 lines
27 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-23
/// 更新时间: 2026-05-26
/// 作用: 承载从通用设置和更多设置中拆出的低频/高级设置项
/// 上次更新: 完善导出/导入设置交互,支持文件选择和确认对话框
/// ============================================================
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' show Divider;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import '../../../../core/router/app_routes.dart';
import '../../../../core/router/app_nav_extension.dart';
import '../../../../core/services/data/data_export_service.dart';
import '../../../../core/services/data/settings_export_service.dart';
import '../../../../core/services/device/haptic_service.dart';
import '../../../../core/services/accessibility/accessibility_service.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/utils/logger.dart';
import '../../../../shared/widgets/adaptive/adaptive_back_button.dart';
import '../providers/general_settings_provider.dart';
import '../services/settings_change_logger.dart';
import 'shared/settings_tiles.dart';
class OtherSettingsPage extends ConsumerStatefulWidget {
const OtherSettingsPage({super.key});
@override
ConsumerState<OtherSettingsPage> createState() => _OtherSettingsPageState();
}
class _OtherSettingsPageState extends ConsumerState<OtherSettingsPage> {
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final settings = ref.watch(generalSettingsProvider);
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
navigationBar: CupertinoNavigationBar(
leading: const AdaptiveBackButton(),
middle: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.gear_solid, size: 18, color: ext.accent),
const SizedBox(width: 6),
Text(
'其他设置',
style: AppTypography.title3.copyWith(color: ext.textPrimary),
),
],
),
backgroundColor: ext.bgElevated.withValues(alpha: 0.85),
border: null,
),
child: SafeArea(
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
children: [
_buildAdvancedSection(ext, settings),
const SizedBox(height: AppSpacing.md),
_buildAccessibilitySection(ext, settings),
const SizedBox(height: AppSpacing.md),
_buildBatteryStorageSection(ext, settings),
const SizedBox(height: AppSpacing.xl),
],
),
),
);
}
// ── 分组1: 高级设置 ──
Widget _buildAdvancedSection(
AppThemeExtension ext,
GeneralSettingsState settings,
) {
return SettingsSectionCard(
header: '高级设置',
icon: CupertinoIcons.wrench_fill,
children: [
SettingsNavigationTile(
icon: CupertinoIcons.ellipsis_circle,
iconColor: const Color(0xFF8E8E93),
title: '更多设置',
subtitle: '内容搜索、存储管理等',
onTap: () {
HapticService.light();
context.appPush(AppRoutes.moreSettings);
},
),
SettingsSwitchTile(
icon: CupertinoIcons.arrow_clockwise,
iconColor: const Color(0xFF8E8E93),
title: '自动检查更新',
subtitle: '启动时自动检查新版本',
value: settings.autoCheckUpdate,
onChanged: (v) {
HapticService.toggleSwitch();
ref.read(generalSettingsProvider.notifier).setAutoCheckUpdate(v);
},
),
SettingsSwitchTile(
icon: CupertinoIcons.arrow_up_arrow_down_circle_fill,
iconColor: const Color(0xFF8E8E93),
title: '同步设置',
subtitle: '跨设备同步设置数据',
value: settings.syncEnabled,
onChanged: (v) {
HapticService.toggleSwitch();
ref.read(generalSettingsProvider.notifier).setSyncEnabled(v);
},
),
SettingsNavigationTile(
icon: CupertinoIcons.list_bullet,
iconColor: const Color(0xFFFF9500),
title: '日志管理',
subtitle: '查看和管理应用日志',
trailing: '${Log.entryCount}',
onTap: () {
HapticService.light();
context.appPush(AppRoutes.logViewer);
},
),
SettingsNavigationTile(
icon: CupertinoIcons.doc_text_search,
iconColor: const Color(0xFF5856D6),
title: '设置变更日志',
subtitle: '查看设置修改记录',
trailing: '${SettingsChangeLogger.entries.length}',
onTap: () {
HapticService.light();
_showSettingsChangeLog(ext);
},
),
SettingsActionTile(
icon: CupertinoIcons.arrow_up_arrow_down_circle_fill,
iconColor: const Color(0xFF5AC8FA),
title: '导出/导入设置',
subtitle: '备份或恢复设置数据',
onTap: () => _showExportImportSheet(ext),
),
SettingsActionTile(
icon: CupertinoIcons.square_arrow_up_fill,
iconColor: const Color(0xFF30D158),
title: '数据导出',
subtitle: '导出应用数据为文件',
onTap: () => _showDataExportDialog(ext),
),
SettingsActionTile(
icon: CupertinoIcons.trash_fill,
iconColor: const Color(0xFFFF9500),
title: '清理缓存',
subtitle: '清理临时缓存数据 (${settings.cacheSizeText})',
onTap: () => _showClearCacheDialog(ext),
),
SettingsActionTile(
icon: CupertinoIcons.exclamationmark_triangle_fill,
iconColor: const Color(0xFFFF3B30),
title: '重置设置',
subtitle: '恢复所有设置为默认值',
isDestructive: true,
onTap: () => _showResetDialog(ext),
),
],
);
}
// ── 分组2: 辅助功能 ──
Widget _buildAccessibilitySection(
AppThemeExtension ext,
GeneralSettingsState settings,
) {
return SettingsSectionCard(
header: '辅助功能',
icon: CupertinoIcons.hand_raised_fill,
children: [
SettingsSwitchTile(
icon: CupertinoIcons.circle_lefthalf_fill,
iconColor: const Color(0xFF8E8E93),
title: '高对比度',
subtitle: '增强界面元素对比度',
value: settings.highContrastEnabled,
onChanged: (v) {
HapticService.toggleSwitch();
ref
.read(generalSettingsProvider.notifier)
.setHighContrastEnabled(v);
},
),
SettingsSelectionTile(
icon: CupertinoIcons.eye_fill,
iconColor: const Color(0xFF30D158),
title: '色弱适配',
subtitle: '调整色彩以适配色弱用户',
value: settings.colorWeakType.label,
onTap: () => _showColorWeakPicker(settings),
),
SettingsSwitchTile(
icon: CupertinoIcons.textformat_abc,
iconColor: const Color(0xFF5856D6),
title: '文字加粗',
subtitle: '增强文字显示粗细',
value: settings.boldTextEnabled,
onChanged: (v) {
HapticService.toggleSwitch();
ref.read(generalSettingsProvider.notifier).setBoldTextEnabled(v);
},
),
SettingsSwitchTile(
icon: CupertinoIcons.eye_fill,
iconColor: const Color(0xFFAF52DE),
title: '语义调试',
subtitle: '显示语义标注覆盖层,辅助开发无障碍适配',
value: settings.semanticsDebugEnabled,
onChanged: (v) {
HapticService.toggleSwitch();
ref
.read(generalSettingsProvider.notifier)
.setSemanticsDebugEnabled(v);
AccessibilityService.instance.setSemanticsDebug(v);
},
),
],
);
}
// ── 分组3: 电池与存储 ──
Widget _buildBatteryStorageSection(
AppThemeExtension ext,
GeneralSettingsState settings,
) {
return SettingsSectionCard(
header: '电池与存储',
icon: CupertinoIcons.battery_25,
children: [
SettingsSwitchTile(
icon: CupertinoIcons.battery_25,
iconColor: const Color(0xFF34C759),
title: '电池优化',
subtitle: '低电量自动开启省流模式',
value: settings.batteryOptimizationEnabled,
onChanged: (v) {
HapticService.toggleSwitch();
ref
.read(generalSettingsProvider.notifier)
.setBatteryOptimizationEnabled(v);
},
),
SettingsSwitchTile(
icon: CupertinoIcons.arrow_clockwise,
iconColor: ext.accent,
title: '后台刷新',
subtitle: '后台获取新内容',
value: settings.backgroundRefreshEnabled,
onChanged: (v) {
HapticService.toggleSwitch();
ref
.read(generalSettingsProvider.notifier)
.setBackgroundRefreshEnabled(v);
},
),
SettingsNavigationTile(
icon: CupertinoIcons.folder_fill,
iconColor: const Color(0xFFFF9500),
title: '存储管理',
subtitle: '管理缓存和数据存储',
onTap: () {
HapticService.light();
context.appPush(AppRoutes.dataManagement);
},
),
],
);
}
// ============================================================
// 选择器弹窗
// ============================================================
void _showColorWeakPicker(GeneralSettingsState settings) {
final ext = AppTheme.ext(context);
HapticService.selection();
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => Container(
height: 260,
decoration: BoxDecoration(
color: ext.bgElevated,
borderRadius: const BorderRadius.vertical(top: Radius.circular(14)),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Text(
'色弱适配',
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
const Divider(height: 1),
Expanded(
child: CupertinoPicker(
itemExtent: 40,
scrollController: FixedExtentScrollController(
initialItem: ColorWeakType.values
.indexWhere((e) => e.id == settings.colorWeakTypeId)
.clamp(0, ColorWeakType.values.length - 1),
),
onSelectedItemChanged: (i) {
ref
.read(generalSettingsProvider.notifier)
.setColorWeakType(ColorWeakType.values[i].id);
},
children: ColorWeakType.values
.map(
(e) => Center(
child: Text(
e.label,
style: AppTypography.body.copyWith(
color: ext.textPrimary,
),
),
),
)
.toList(),
),
),
],
),
),
);
}
// ============================================================
// 操作对话框
// ============================================================
void _showClearCacheDialog(AppThemeExtension ext) {
HapticService.impact();
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.trash_fill, size: 18, color: Color(0xFFFF9500)),
SizedBox(width: 8),
Text('清理缓存'),
],
),
content: const Text('确定要清理所有临时缓存数据吗?'),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: const Text('清理'),
onPressed: () {
ref.read(generalSettingsProvider.notifier).clearCache();
Navigator.pop(ctx);
},
),
],
),
);
}
void _showResetDialog(AppThemeExtension ext) {
HapticService.heavy();
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.exclamationmark_triangle_fill,
size: 18,
color: Color(0xFFFF3B30),
),
SizedBox(width: 8),
Text('重置设置'),
],
),
content: const Text('确定要将所有设置恢复为默认值吗?此操作不可撤销。'),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: const Text('重置'),
onPressed: () {
ref.read(generalSettingsProvider.notifier).resetAll();
Navigator.pop(ctx);
},
),
],
),
);
}
void _showExportImportSheet(AppThemeExtension ext) {
HapticService.light();
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => Container(
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: ext.bgElevated,
borderRadius: const BorderRadius.vertical(top: Radius.circular(14)),
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'📤 导出/导入设置',
style: AppTypography.headline.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppSpacing.sm),
Text(
'导出当前设置到JSON文件或从文件恢复设置',
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.lg),
SizedBox(
width: double.infinity,
child: CupertinoButton(
color: ext.accent,
borderRadius: AppRadius.mdBorder,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.square_arrow_up,
size: 18,
color: CupertinoColors.white,
),
const SizedBox(width: AppSpacing.sm),
Text(
'导出设置',
style: AppTypography.body.copyWith(
color: CupertinoColors.white,
),
),
],
),
onPressed: () async {
Navigator.pop(ctx);
try {
await SettingsExportService.shareSettings();
} catch (e) {
if (mounted) {
_showMessage('导出失败: $e');
}
}
},
),
),
const SizedBox(height: AppSpacing.md),
SizedBox(
width: double.infinity,
child: CupertinoButton(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.square_arrow_down,
size: 18,
color: ext.textPrimary,
),
const SizedBox(width: AppSpacing.sm),
Text(
'从文件导入',
style: AppTypography.body.copyWith(
color: ext.textPrimary,
),
),
],
),
onPressed: () {
Navigator.pop(ctx);
_importFromFile(ext);
},
),
),
const SizedBox(height: AppSpacing.sm),
SizedBox(
width: double.infinity,
child: CupertinoButton(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.doc_on_clipboard,
size: 18,
color: ext.textPrimary,
),
const SizedBox(width: AppSpacing.sm),
Text(
'从剪贴板导入',
style: AppTypography.body.copyWith(
color: ext.textPrimary,
),
),
],
),
onPressed: () {
Navigator.pop(ctx);
_showImportFromClipboardDialog(ext);
},
),
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
child: Text(
'取消',
style: AppTypography.body.copyWith(color: ext.textHint),
),
onPressed: () => Navigator.pop(ctx),
),
],
),
),
),
);
}
void _importFromFile(AppThemeExtension ext) async {
HapticService.light();
try {
FilePickerResult? result;
try {
result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
} catch (_) {
result = await FilePicker.pickFiles();
}
if (result == null || result.files.isEmpty) return;
final filePath = result.files.single.path;
if (filePath == null || filePath.isEmpty) {
if (mounted) _showMessage('文件路径无效');
return;
}
final file = File(filePath);
if (!await file.exists()) {
if (mounted) _showMessage('文件不存在');
return;
}
final jsonStr = await file.readAsString();
if (jsonStr.isEmpty) {
if (mounted) _showMessage('文件内容为空');
return;
}
if (!mounted) return;
_showImportConfirmDialog(ext, jsonStr);
} catch (e) {
Log.e('文件选择失败', e);
if (mounted) _showMessage('文件选择失败: $e');
}
}
void _showImportFromClipboardDialog(AppThemeExtension ext) {
final controller = TextEditingController();
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.doc_on_clipboard, size: 18),
SizedBox(width: 8),
Text('从剪贴板导入'),
],
),
content: Padding(
padding: const EdgeInsets.only(top: AppSpacing.sm),
child: CupertinoTextField(
controller: controller,
maxLines: 6,
placeholder: '粘贴导出的 JSON 数据',
style: AppTypography.caption1.copyWith(
color: ext.textPrimary,
fontFamily: 'monospace',
),
),
),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
onPressed: () {
controller.dispose();
Navigator.pop(ctx);
},
),
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('确认导入'),
onPressed: () {
final json = controller.text;
controller.dispose();
Navigator.pop(ctx);
if (json.isNotEmpty) {
_showImportConfirmDialog(ext, json);
}
},
),
],
),
);
}
void _showImportConfirmDialog(AppThemeExtension ext, String jsonStr) {
HapticService.impact();
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.exclamationmark_triangle_fill,
size: 18,
color: Color(0xFFFF9500),
),
SizedBox(width: 8),
Text('确认导入'),
],
),
content: const Text('导入设置将覆盖当前设置,此操作不可撤销。确定要继续吗?'),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: const Text('导入'),
onPressed: () async {
Navigator.pop(ctx);
final success = await SettingsExportService.importFromJson(
jsonStr,
);
if (mounted) {
_showMessage(success ? '✅ 导入成功,设置已更新' : '❌ 导入失败,请检查文件格式');
if (success) {
ref.read(generalSettingsProvider.notifier).reload();
}
}
},
),
],
),
);
}
void _showDataExportDialog(AppThemeExtension ext) {
HapticService.light();
try {
DataExportService.shareData();
} catch (e) {
if (mounted) {
_showMessage('数据导出失败: $e');
}
}
}
void _showSettingsChangeLog(AppThemeExtension ext) {
final entries = SettingsChangeLogger.entries;
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.doc_text_search, size: 18, color: ext.accent),
const SizedBox(width: 8),
const Text('设置变更日志'),
],
),
content: SizedBox(
width: double.maxFinite,
height: 320,
child: entries.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.doc_text,
size: 36,
color: ext.textHint,
),
const SizedBox(height: AppSpacing.sm),
Text(
'暂无变更记录',
style: AppTypography.subhead.copyWith(
color: ext.textHint,
),
),
],
),
)
: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: entries.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, index) {
final entry = entries[index];
final time =
'${entry.changedAt.month}/${entry.changedAt.day} '
'${entry.changedAt.hour.toString().padLeft(2, '0')}:'
'${entry.changedAt.minute.toString().padLeft(2, '0')}';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
entry.key,
style: AppTypography.caption1.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
time,
style: AppTypography.caption2.copyWith(
color: ext.textHint,
),
),
],
),
const SizedBox(height: 2),
Text(
'${entry.oldValue}${entry.newValue}',
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
],
),
);
},
),
),
actions: [
if (entries.isNotEmpty)
CupertinoDialogAction(
isDestructiveAction: true,
child: const Text('清空日志'),
onPressed: () async {
await SettingsChangeLogger.clear();
if (!context.mounted) return;
Navigator.pop(ctx);
setState(() {});
},
),
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('关闭'),
onPressed: () => Navigator.pop(ctx),
),
],
),
);
}
void _showMessage(String message) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
content: Text(message),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('好的'),
onPressed: () => Navigator.pop(ctx),
),
],
),
);
}
}