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

233 lines
6.4 KiB
Dart
Raw Permalink 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.
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,
],
),
);
}
}