refactor: 拆分主页与公共组件,扫码登录改走 WinHTTP
- 抽出 lib/core 的日期/JSON 工具,收敛各页重复解析 - 主页与音乐风险页拆成组件,风险功能共用一套运行框架 - 修复设置页作为主页页签保存时弹掉根路由导致的黑屏 - 登录对齐 Astral aimedb 协议:84 位二维码校验、签名与请求头修正, 补 GlobalSign 中间证书;Windows 改用 WinHTTP 绕过服务端对 BoringSSL 的 TLS 指纹拦截(证书校验保持开启) - 补开发预览数据与测试
This commit is contained in:
1 parent
0cb5320290
commit
e9357f5969
55 files changed
+4387
-3392
No files matched your search
@@ -0,0 +1,232 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../config/strings.dart';
|
||||
import '../app_card.dart';
|
||||
import 'risk_list_widgets.dart';
|
||||
import 'risk_widgets.dart';
|
||||
|
||||
/// 一行待提交的收藏品道具(对应 wire 上的 UserItem)。
|
||||
class PendingItem {
|
||||
final int kind;
|
||||
final int id;
|
||||
|
||||
const PendingItem(this.kind, this.id);
|
||||
}
|
||||
|
||||
/// 一行待提交的歌曲解锁,展开成 itemKind 5/6/7 的若干行。
|
||||
class PendingMusic {
|
||||
final int musicId;
|
||||
final bool base;
|
||||
final bool master;
|
||||
final bool remaster;
|
||||
|
||||
const PendingMusic(this.musicId, this.base, this.master, this.remaster);
|
||||
|
||||
/// 难度由 itemKind 表达,不写在 musicDetail 上。
|
||||
List<int> get itemKinds => [
|
||||
if (base) 5,
|
||||
if (master) 6,
|
||||
if (remaster) 7,
|
||||
];
|
||||
|
||||
String get optionLabel => [
|
||||
if (base) '歌曲',
|
||||
if (master) 'Master',
|
||||
if (remaster) 'Re:Master',
|
||||
].join('/');
|
||||
|
||||
/// ConstParameter.DxScoreBaseID=10000 / StrongScoreBaseID=20000
|
||||
/// (`MAI2System/ConstParameter.cs:168,171`)。这段里的歌 Master 需要
|
||||
/// 显式 itemKind 6,不像 id < 10000 那样在 IsUnlockMaster 里直接放过。
|
||||
bool get isDeluxeRange => musicId >= 10000 && musicId < 20000;
|
||||
}
|
||||
|
||||
/// 歌曲解锁的输入卡。输入框与三个难度勾选由本组件持有,
|
||||
/// 校验通过后把待提交行交给父级;父级返回 false 表示重复、保留输入。
|
||||
class UnlockMusicEditorCard extends StatefulWidget {
|
||||
final bool enabled;
|
||||
final bool Function(PendingMusic music) onAdd;
|
||||
final ValueChanged<String> onError;
|
||||
final Widget pendingList;
|
||||
final bool showDxNotice;
|
||||
|
||||
const UnlockMusicEditorCard({
|
||||
super.key,
|
||||
required this.enabled,
|
||||
required this.onAdd,
|
||||
required this.onError,
|
||||
required this.pendingList,
|
||||
required this.showDxNotice,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UnlockMusicEditorCard> createState() => _UnlockMusicEditorCardState();
|
||||
}
|
||||
|
||||
class _UnlockMusicEditorCardState extends State<UnlockMusicEditorCard> {
|
||||
final _controller = TextEditingController();
|
||||
bool _music = false;
|
||||
bool _master = false;
|
||||
bool _remaster = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _add() {
|
||||
final id = int.tryParse(_controller.text.trim()) ?? -1;
|
||||
if (id <= 0) {
|
||||
widget.onError(AppStrings.unlockNeedMusicId);
|
||||
return;
|
||||
}
|
||||
if (!_music && !_master && !_remaster) {
|
||||
widget.onError(AppStrings.unlockNeedOption);
|
||||
return;
|
||||
}
|
||||
|
||||
final added = widget.onAdd(PendingMusic(id, _music, _master, _remaster));
|
||||
if (!added) return;
|
||||
|
||||
setState(() {
|
||||
_controller.clear();
|
||||
_music = _master = _remaster = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final enabled = widget.enabled;
|
||||
|
||||
return AppCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RiskNumberField(
|
||||
controller: _controller,
|
||||
label: AppStrings.unlockMusicIdLabel,
|
||||
hint: AppStrings.unlockMusicIdHint,
|
||||
enabled: enabled,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
RiskCheckRow(
|
||||
value: _music,
|
||||
label: AppStrings.unlockMusicOption,
|
||||
enabled: enabled,
|
||||
onChanged: (v) => setState(() => _music = v),
|
||||
),
|
||||
RiskCheckRow(
|
||||
value: _master,
|
||||
label: AppStrings.unlockMasterOption,
|
||||
enabled: enabled,
|
||||
onChanged: (v) => setState(() => _master = v),
|
||||
),
|
||||
RiskCheckRow(
|
||||
value: _remaster,
|
||||
label: AppStrings.unlockRemasterOption,
|
||||
enabled: enabled,
|
||||
onChanged: (v) => setState(() => _remaster = v),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RiskAddButton(onPressed: enabled ? _add : null),
|
||||
widget.pendingList,
|
||||
// DX 段的 Master / Re:Master 不白送,勾选项里漏一个就开不出来,
|
||||
// 这是这段 ID 最容易踩的坑,只在列表真含 DX 曲时才说明。
|
||||
if (widget.showDxNotice) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
AppStrings.unlockDxRangeNotice,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 收藏品获取的输入卡(类型下拉 + ID)。
|
||||
class CollectiblesEditorCard extends StatefulWidget {
|
||||
final bool enabled;
|
||||
final bool Function(PendingItem item) onAdd;
|
||||
final ValueChanged<String> onError;
|
||||
final Widget pendingList;
|
||||
|
||||
const CollectiblesEditorCard({
|
||||
super.key,
|
||||
required this.enabled,
|
||||
required this.onAdd,
|
||||
required this.onError,
|
||||
required this.pendingList,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CollectiblesEditorCard> createState() => _CollectiblesEditorCardState();
|
||||
}
|
||||
|
||||
class _CollectiblesEditorCardState extends State<CollectiblesEditorCard> {
|
||||
final _controller = TextEditingController();
|
||||
int _kind = 3;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _add() {
|
||||
final id = int.tryParse(_controller.text.trim()) ?? -1;
|
||||
if (id <= 0) {
|
||||
widget.onError(AppStrings.collectiblesNeedItemId);
|
||||
return;
|
||||
}
|
||||
|
||||
final added = widget.onAdd(PendingItem(_kind, id));
|
||||
if (!added) return;
|
||||
|
||||
setState(() => _controller.clear());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enabled = widget.enabled;
|
||||
|
||||
return AppCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RiskDropdownField<int>(
|
||||
value: _kind,
|
||||
label: AppStrings.collectiblesItemKindLabel,
|
||||
enabled: enabled,
|
||||
items: [
|
||||
for (final kind in AppStrings.collectiblesItemKinds)
|
||||
DropdownMenuItem(
|
||||
value: kind,
|
||||
child: Text(
|
||||
'${AppStrings.collectiblesItemKindName(kind)} ($kind)',
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => setState(() => _kind = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
RiskNumberField(
|
||||
controller: _controller,
|
||||
label: AppStrings.collectiblesItemIdLabel,
|
||||
hint: AppStrings.collectiblesItemIdHint,
|
||||
enabled: enabled,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RiskAddButton(onPressed: enabled ? _add : null),
|
||||
widget.pendingList,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user