- 抽出 lib/core 的日期/JSON 工具,收敛各页重复解析 - 主页与音乐风险页拆成组件,风险功能共用一套运行框架 - 修复设置页作为主页页签保存时弹掉根路由导致的黑屏 - 登录对齐 Astral aimedb 协议:84 位二维码校验、签名与请求头修正, 补 GlobalSign 中间证书;Windows 改用 WinHTTP 绕过服务端对 BoringSSL 的 TLS 指纹拦截(证书校验保持开启) - 补开发预览数据与测试
121 lines
3.6 KiB
Dart
121 lines
3.6 KiB
Dart
import 'dart:convert';
|
||
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
class TitleServerConfig {
|
||
final String titleServerUrl;
|
||
final String aesKey;
|
||
final String aesIv;
|
||
final String obfuscateParam;
|
||
final String apiVersion;
|
||
final String clientId;
|
||
final int regionId;
|
||
final int placeId;
|
||
final String regionName;
|
||
final String placeName;
|
||
final String keychipId;
|
||
|
||
const TitleServerConfig({
|
||
required this.titleServerUrl,
|
||
required this.aesKey,
|
||
required this.aesIv,
|
||
this.obfuscateParam = '',
|
||
this.apiVersion = '',
|
||
required this.clientId,
|
||
this.regionId = 0,
|
||
this.placeId = 0,
|
||
this.regionName = '',
|
||
this.placeName = '',
|
||
this.keychipId = '',
|
||
});
|
||
|
||
List<int> get aesKeyBytes => utf8.encode(aesKey);
|
||
List<int> get aesIvBytes => utf8.encode(aesIv);
|
||
|
||
/// 扫码登录签的是**完整 Keychip**(`A63E-01C30471007`),游戏服务器那边才用
|
||
/// 11 位短值。这里按 Astral 的约定补出完整形式:前 4 位 + 连字符 + 余下 + placeId,
|
||
/// 所以只填了短值也能还原;填不满 11 位就当没配。
|
||
String get fullKeychipId {
|
||
final raw = keychipId.replaceAll(RegExp(r'[\s\-]'), '').toUpperCase();
|
||
if (raw.length < 11) return '';
|
||
final short = '${raw.substring(0, 4)}-${raw.substring(4)}';
|
||
return raw.length == 11 ? '$short$placeId' : short;
|
||
}
|
||
|
||
Map<String, dynamic> toJson() => {
|
||
'titleServerUrl': titleServerUrl,
|
||
'aesKey': aesKey,
|
||
'aesIv': aesIv,
|
||
'obfuscateParam': obfuscateParam,
|
||
'apiVersion': apiVersion,
|
||
'clientId': clientId,
|
||
'regionId': regionId,
|
||
'placeId': placeId,
|
||
'regionName': regionName,
|
||
'placeName': placeName,
|
||
'keychipId': keychipId,
|
||
};
|
||
|
||
factory TitleServerConfig.fromJson(Map<String, dynamic> json) {
|
||
return TitleServerConfig(
|
||
titleServerUrl: json['titleServerUrl'] as String? ?? '',
|
||
aesKey: json['aesKey'] as String? ?? '',
|
||
aesIv: json['aesIv'] as String? ?? '',
|
||
obfuscateParam: json['obfuscateParam'] as String? ?? '',
|
||
apiVersion: json['apiVersion'] as String? ?? '',
|
||
clientId: json['clientId'] as String? ?? '',
|
||
regionId: json['regionId'] as int? ?? 0,
|
||
placeId: json['placeId'] as int? ?? 0,
|
||
regionName: json['regionName'] as String? ?? '',
|
||
placeName: json['placeName'] as String? ?? '',
|
||
keychipId: json['keychipId'] as String? ?? '',
|
||
);
|
||
}
|
||
}
|
||
|
||
class TitleServerConfigHolder {
|
||
static const _storageKey = 'title_server_config';
|
||
|
||
static final TitleServerConfigHolder _instance = TitleServerConfigHolder._();
|
||
factory TitleServerConfigHolder() => _instance;
|
||
TitleServerConfigHolder._();
|
||
|
||
TitleServerConfig? _config;
|
||
TitleServerConfig? get config => _config;
|
||
|
||
bool get isConfigured {
|
||
final c = _config;
|
||
return c != null &&
|
||
c.titleServerUrl.isNotEmpty &&
|
||
c.aesKey.isNotEmpty &&
|
||
c.aesIv.isNotEmpty &&
|
||
c.clientId.isNotEmpty;
|
||
}
|
||
|
||
Future<void> init() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final raw = prefs.getString(_storageKey);
|
||
if (raw != null) {
|
||
try {
|
||
_config = TitleServerConfig.fromJson(
|
||
jsonDecode(raw) as Map<String, dynamic>,
|
||
);
|
||
} catch (_) {
|
||
_config = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> update(TitleServerConfig config) async {
|
||
_config = config;
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setString(_storageKey, jsonEncode(config.toJson()));
|
||
}
|
||
|
||
Future<void> clear() async {
|
||
_config = null;
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.remove(_storageKey);
|
||
}
|
||
}
|