feat.Hatsune Miku

This commit is contained in:
Cookiues committed 2026-05-29 16:21:54 +08:00
1 parent 53e078ff91
commit cd6491cdc5
5 files changed
+166 -51

No files matched your search

+9 -14
View File
@@ -11,7 +11,6 @@ class AppStrings {
static const appTitle = 'Project Ichino'; static const appTitle = 'Project Ichino';
static const loginSubtitle = '扫描或输入二维码进行登录'; static const loginSubtitle = '扫描或输入二维码进行登录';
static const qrCodeToken = 'QR Code 令牌'; static const qrCodeToken = 'QR Code 令牌';
static const qrHint = '粘贴二维码解析内容...';
static const qrParsedResult = '解析结果(截取后 64 位)'; static const qrParsedResult = '解析结果(截取后 64 位)';
static const uploadQR = '上传二维码'; static const uploadQR = '上传二维码';
static const login = '登录'; static const login = '登录';
@@ -124,33 +123,29 @@ class AppStrings {
static const machineSettings = '机器信息'; static const machineSettings = '机器信息';
static const save = '保存'; static const save = '保存';
static const labelTitleServerUrl = 'Title Server URL'; static const labelTitleServerUrl = 'Title Server URL';
static const hintTitleServerUrl = 'https://maimai-gm.wahlap.com:42081/Maimai2Servlet/';
static const labelAesKey = 'AES Key'; static const labelAesKey = 'AES Key';
static const hintAesKey = 'your aes key string';
static const labelAesIv = 'AES IV'; static const labelAesIv = 'AES IV';
static const hintAesIv = 'your aes iv string';
static const labelClientId = 'Client ID'; static const labelClientId = 'Client ID';
static const hintClientId = 'A63E01C2805';
static const labelRegionId = 'Region ID'; static const labelRegionId = 'Region ID';
static const hintRegionId = '1';
static const labelPlaceId = 'Place ID'; static const labelPlaceId = 'Place ID';
static const hintPlaceId = '1403';
static const labelObfuscateParam = 'Obfuscate Param'; static const labelObfuscateParam = 'Obfuscate Param';
static const hintObfuscateParam = 'LatuAa81';
static const labelApiVersion = 'API Version (Mai-Encoding)'; static const labelApiVersion = 'API Version (Mai-Encoding)';
static const hintApiVersion = '1.53';
static const labelRegionName = 'Region 名称'; static const labelRegionName = 'Region 名称';
static const hintRegionName = '北京';
static const labelPlaceName = 'Place 名称'; static const labelPlaceName = 'Place 名称';
static const hintPlaceName = '插电师北京王府井银泰店';
static const labelKeychipId = 'Keychip ID'; static const labelKeychipId = 'Keychip ID';
static const hintKeychipId = 'A63E-01C28055905';
static const labelAimeUrl = 'Aime URL'; static const labelAimeUrl = 'Aime URL';
static const hintAimeUrl = 'http://ai.sys-allnet.cn/wc_aime/api/get_data';
static const labelAimeSalt = 'Aime Salt'; static const labelAimeSalt = 'Aime Salt';
static const hintAimeSalt = 'API Key 派生用 Salt';
static String fieldRequired(String label) => '$label 不能为空'; static String fieldRequired(String label) => '$label 不能为空';
// Config Export/Import
static const configExportImport = '配置导入/导出';
static const exportConfig = '导出当前配置';
static const importConfig = '从剪贴板导入配置';
static const exportSuccess = '配置已导出到剪贴板 (Base64)';
static const importSuccess = '配置已导入并保存';
static const importFailed = '导入失败:剪贴板内容无效';
static const noConfigToExport = '没有可导出的配置';
// About // About
static const aboutTitle = 'Project Ichino'; static const aboutTitle = 'Project Ichino';
static const aboutDesc = '基于 QR Code 的 maimai DX 街机网络登录客户端。'; static const aboutDesc = '基于 QR Code 的 maimai DX 街机网络登录客户端。';
+10 -10
View File
@@ -22,18 +22,18 @@ class TitleServerConfig {
required this.titleServerUrl, required this.titleServerUrl,
required this.aesKey, required this.aesKey,
required this.aesIv, required this.aesIv,
this.obfuscateParam = 'LatuAa81', this.obfuscateParam = '',
this.apiVersion = '1.53', this.apiVersion = '',
required this.clientId, required this.clientId,
this.regionId = 1, this.regionId = 0,
this.placeId = 1403, this.placeId = 0,
this.regionName = '', this.regionName = '',
this.placeName = '', this.placeName = '',
this.keychipId = '', this.keychipId = '',
this.aimeUrl = '', this.aimeUrl = '',
this.aimeSalt = '', this.aimeSalt = '',
String? openGameID, String? openGameID,
}) : openGameID = openGameID ?? 'MAID'; }) : openGameID = openGameID ?? '';
List<int> get aesKeyBytes => utf8.encode(aesKey); List<int> get aesKeyBytes => utf8.encode(aesKey);
List<int> get aesIvBytes => utf8.encode(aesIv); List<int> get aesIvBytes => utf8.encode(aesIv);
@@ -60,17 +60,17 @@ class TitleServerConfig {
titleServerUrl: json['titleServerUrl'] as String? ?? '', titleServerUrl: json['titleServerUrl'] as String? ?? '',
aesKey: json['aesKey'] as String? ?? '', aesKey: json['aesKey'] as String? ?? '',
aesIv: json['aesIv'] as String? ?? '', aesIv: json['aesIv'] as String? ?? '',
obfuscateParam: json['obfuscateParam'] as String? ?? 'LatuAa81', obfuscateParam: json['obfuscateParam'] as String? ?? '',
apiVersion: json['apiVersion'] as String? ?? '1.53', apiVersion: json['apiVersion'] as String? ?? '',
clientId: json['clientId'] as String? ?? '', clientId: json['clientId'] as String? ?? '',
regionId: json['regionId'] as int? ?? 1, regionId: json['regionId'] as int? ?? 0,
placeId: json['placeId'] as int? ?? 1403, placeId: json['placeId'] as int? ?? 0,
regionName: json['regionName'] as String? ?? '', regionName: json['regionName'] as String? ?? '',
placeName: json['placeName'] as String? ?? '', placeName: json['placeName'] as String? ?? '',
keychipId: json['keychipId'] as String? ?? '', keychipId: json['keychipId'] as String? ?? '',
aimeUrl: json['aimeUrl'] as String? ?? '', aimeUrl: json['aimeUrl'] as String? ?? '',
aimeSalt: json['aimeSalt'] as String? ?? '', aimeSalt: json['aimeSalt'] as String? ?? '',
openGameID: json['openGameID'] as String? ?? 'MAID', openGameID: json['openGameID'] as String? ?? '',
); );
} }
} }
-6
View File
@@ -227,12 +227,6 @@ class _LoginPageState extends State<LoginPage> {
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppStrings.qrHint,
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant
.withValues(alpha: 0.5),
fontFamily: 'monospace',
),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
+111
View File
@@ -0,0 +1,111 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../config/strings.dart';
import '../config/title_server_config.dart';
class ConfigExportImportPage extends StatelessWidget {
const ConfigExportImportPage({super.key});
Future<void> _exportConfig(BuildContext context) async {
final config = TitleServerConfigHolder().config;
if (config == null) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text(AppStrings.noConfigToExport)),
);
}
return;
}
final json = jsonEncode(config.toJson());
final base64 = base64Encode(utf8.encode(json));
await Clipboard.setData(ClipboardData(text: base64));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text(AppStrings.exportSuccess)),
);
}
}
Future<void> _importConfig(BuildContext context) async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text;
if (text == null || text.isEmpty) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text(AppStrings.importFailed)),
);
}
return;
}
try {
final json = utf8.decode(base64Decode(text));
final map = jsonDecode(json) as Map<String, dynamic>;
final config = TitleServerConfig.fromJson(map);
await TitleServerConfigHolder().update(config);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text(AppStrings.importSuccess)),
);
}
} catch (_) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text(AppStrings.importFailed)),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(AppStrings.configExportImport),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton.icon(
onPressed: () => _exportConfig(context),
icon: const Icon(Icons.upload, size: 20),
label: const Text(AppStrings.exportConfig),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: () => _importConfig(context),
icon: const Icon(Icons.download, size: 20),
label: const Text(AppStrings.importConfig),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
),
),
),
);
}
}
+36 -21
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../config/strings.dart'; import '../config/strings.dart';
import '../config/title_server_config.dart'; import '../config/title_server_config.dart';
import 'config_export_import_page.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
final bool showAppBar; final bool showAppBar;
@@ -18,10 +19,10 @@ class _SettingsPageState extends State<SettingsPage> {
final _aesKeyController = TextEditingController(); final _aesKeyController = TextEditingController();
final _aesIvController = TextEditingController(); final _aesIvController = TextEditingController();
final _clientIdController = TextEditingController(); final _clientIdController = TextEditingController();
final _regionIdController = TextEditingController(text: '1'); final _regionIdController = TextEditingController();
final _placeIdController = TextEditingController(text: '1403'); final _placeIdController = TextEditingController();
final _obfuscateController = TextEditingController(text: 'LatuAa81'); final _obfuscateController = TextEditingController();
final _apiVersionController = TextEditingController(text: '1.53'); final _apiVersionController = TextEditingController();
final _regionNameController = TextEditingController(); final _regionNameController = TextEditingController();
final _placeNameController = TextEditingController(); final _placeNameController = TextEditingController();
final _keychipIdController = TextEditingController(); final _keychipIdController = TextEditingController();
@@ -75,8 +76,8 @@ class _SettingsPageState extends State<SettingsPage> {
aesKey: _aesKeyController.text.trim(), aesKey: _aesKeyController.text.trim(),
aesIv: _aesIvController.text.trim(), aesIv: _aesIvController.text.trim(),
clientId: _clientIdController.text.trim(), clientId: _clientIdController.text.trim(),
regionId: int.tryParse(_regionIdController.text.trim()) ?? 1, regionId: int.tryParse(_regionIdController.text.trim()) ?? 0,
placeId: int.tryParse(_placeIdController.text.trim()) ?? 1403, placeId: int.tryParse(_placeIdController.text.trim()) ?? 0,
obfuscateParam: _obfuscateController.text.trim(), obfuscateParam: _obfuscateController.text.trim(),
apiVersion: _apiVersionController.text.trim(), apiVersion: _apiVersionController.text.trim(),
regionName: _regionNameController.text.trim(), regionName: _regionNameController.text.trim(),
@@ -109,35 +110,35 @@ class _SettingsPageState extends State<SettingsPage> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildSection(theme, AppStrings.titleServerSection, [ _buildSection(theme, AppStrings.titleServerSection, [
_buildField(controller: _urlController, label: AppStrings.labelTitleServerUrl, hint: AppStrings.hintTitleServerUrl), _buildField(controller: _urlController, label: AppStrings.labelTitleServerUrl),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _aesKeyController, label: AppStrings.labelAesKey, hint: AppStrings.hintAesKey), _buildField(controller: _aesKeyController, label: AppStrings.labelAesKey),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _aesIvController, label: AppStrings.labelAesIv, hint: AppStrings.hintAesIv), _buildField(controller: _aesIvController, label: AppStrings.labelAesIv),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _clientIdController, label: AppStrings.labelClientId, hint: AppStrings.hintClientId), _buildField(controller: _clientIdController, label: AppStrings.labelClientId),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _obfuscateController, label: AppStrings.labelObfuscateParam, hint: AppStrings.hintObfuscateParam), _buildField(controller: _obfuscateController, label: AppStrings.labelObfuscateParam),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _apiVersionController, label: AppStrings.labelApiVersion, hint: AppStrings.hintApiVersion), _buildField(controller: _apiVersionController, label: AppStrings.labelApiVersion),
]), ]),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildSection(theme, AppStrings.machineSettings, [ _buildSection(theme, AppStrings.machineSettings, [
_buildField(controller: _regionIdController, label: AppStrings.labelRegionId, hint: AppStrings.hintRegionId), _buildField(controller: _regionIdController, label: AppStrings.labelRegionId),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _regionNameController, label: AppStrings.labelRegionName, hint: AppStrings.hintRegionName), _buildField(controller: _regionNameController, label: AppStrings.labelRegionName),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _placeIdController, label: AppStrings.labelPlaceId, hint: AppStrings.hintPlaceId), _buildField(controller: _placeIdController, label: AppStrings.labelPlaceId),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _placeNameController, label: AppStrings.labelPlaceName, hint: AppStrings.hintPlaceName), _buildField(controller: _placeNameController, label: AppStrings.labelPlaceName),
]), ]),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildSection(theme, AppStrings.authServerSettings, [ _buildSection(theme, AppStrings.authServerSettings, [
_buildField(controller: _keychipIdController, label: AppStrings.labelKeychipId, hint: AppStrings.hintKeychipId), _buildField(controller: _keychipIdController, label: AppStrings.labelKeychipId),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _aimeUrlController, label: AppStrings.labelAimeUrl, hint: AppStrings.hintAimeUrl), _buildField(controller: _aimeUrlController, label: AppStrings.labelAimeUrl),
const SizedBox(height: 14), const SizedBox(height: 14),
_buildField(controller: _aimeSaltController, label: AppStrings.labelAimeSalt, hint: AppStrings.hintAimeSalt), _buildField(controller: _aimeSaltController, label: AppStrings.labelAimeSalt),
]), ]),
const SizedBox(height: 32), const SizedBox(height: 32),
FilledButton.icon( FilledButton.icon(
@@ -151,6 +152,22 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
), ),
), ),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ConfigExportImportPage()),
);
},
icon: const Icon(Icons.import_export, size: 20),
label: const Text(AppStrings.configExportImport),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
], ],
), ),
), ),
@@ -191,13 +208,11 @@ class _SettingsPageState extends State<SettingsPage> {
Widget _buildField({ Widget _buildField({
required TextEditingController controller, required TextEditingController controller,
required String label, required String label,
required String hint,
}) { }) {
return TextFormField( return TextFormField(
controller: controller, controller: controller,
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: label,
hintText: hint,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),