Files
Ichino/lib/widgets/account_backup_card.dart
T
Bad0RANG3 e9357f5969 refactor: 拆分主页与公共组件,扫码登录改走 WinHTTP
- 抽出 lib/core 的日期/JSON 工具,收敛各页重复解析
- 主页与音乐风险页拆成组件,风险功能共用一套运行框架
- 修复设置页作为主页页签保存时弹掉根路由导致的黑屏
- 登录对齐 Astral aimedb 协议:84 位二维码校验、签名与请求头修正,
  补 GlobalSign 中间证书;Windows 改用 WinHTTP 绕过服务端对
  BoringSSL 的 TLS 指纹拦截(证书校验保持开启)
- 补开发预览数据与测试
2026-10-25 15:51:10 +08:00

188 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show Clipboard, ClipboardData;
import '../config/strings.dart';
import '../models/session_model.dart';
import '../services/account_backup_service.dart';
import '../services/title_api_service.dart';
import 'app_card.dart';
import 'app_notice.dart';
/// 账号备份:把服务器能读到的用户数据汇成一长串 JSON,可复制到剪贴板。
///
/// 只读不写——全程只发 `GetUser*Api`,不会碰 UpsertUserAllApi,所以和
/// 「风险」页那些伪造包相比是安全操作。
class AccountBackupCard extends StatefulWidget {
final int userId;
final String? cookies;
const AccountBackupCard({super.key, required this.userId, this.cookies});
@override
State<AccountBackupCard> createState() => _AccountBackupCardState();
}
class _AccountBackupCardState extends State<AccountBackupCard> {
bool _running = false;
String _progress = '';
AccountBackup? _result;
String? _error;
Future<void> _run() async {
final service = SessionModel.instance.createTitleService(
cookies: widget.cookies,
);
if (service == null) {
context.showSnack(AppStrings.ticketNotConfigured);
return;
}
setState(() {
_running = true;
_error = null;
_result = null;
_progress = '';
});
try {
final backup = await AccountBackupService(service, userId: widget.userId)
.run(
onSection: (section) {
if (mounted) setState(() => _progress = section);
},
);
if (!mounted) return;
setState(() {
_result = backup;
_running = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = describeApiError(e);
_running = false;
});
}
}
Future<void> _copy(String text) async {
await Clipboard.setData(ClipboardData(text: text));
if (!mounted) return;
context.showSnack(AppStrings.accountBackupCopied);
}
String _sizeLabel(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / 1024 / 1024).toStringAsFixed(2)} MB';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final result = _result;
return AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(
icon: Icons.folder_zip_outlined,
title: AppStrings.accountBackupTitle,
),
const SizedBox(height: 10),
Text(
AppStrings.accountBackupDesc,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
child: FilledButton.tonalIcon(
onPressed: _running ? null : _run,
icon: _running
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.download_outlined, size: 20),
label: Text(
_running
? AppStrings.accountBackupRunning(_progress)
: AppStrings.accountBackupRun,
),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 13),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
if (_error != null) ...[
const SizedBox(height: 12),
AppNotice(_error!, icon: Icons.error_outline, error: true),
],
if (result != null) ...[
const SizedBox(height: 12),
if (!result.isComplete) ...[
AppNotice(
AppStrings.accountBackupPartial(result.failedSections),
icon: Icons.warning_amber_rounded,
),
const SizedBox(height: 10),
],
Row(
children: [
Expanded(
child: Text(
AppStrings.accountBackupSize(_sizeLabel(result.byteLength)),
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
TextButton.icon(
onPressed: () => _copy(result.encode()),
icon: const Icon(Icons.copy_all, size: 18),
label: const Text(AppStrings.accountBackupCopy),
),
],
),
Container(
width: double.infinity,
constraints: const BoxConstraints(maxHeight: 260),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withValues(
alpha: 0.5,
),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: SelectableText(
result.encode(),
style: theme.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
fontSize: 11,
),
),
),
),
const SizedBox(height: 10),
Text(
AppStrings.accountBackupSecurity,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
],
],
),
);
}
}