feat.Hatsune Miku
This commit is contained in:
1 parent
53e078ff91
commit
cd6491cdc5
5 files changed
+166
-51
No files matched your search
+9
-14
@@ -11,7 +11,6 @@ class AppStrings {
|
||||
static const appTitle = 'Project Ichino';
|
||||
static const loginSubtitle = '扫描或输入二维码进行登录';
|
||||
static const qrCodeToken = 'QR Code 令牌';
|
||||
static const qrHint = '粘贴二维码解析内容...';
|
||||
static const qrParsedResult = '解析结果(截取后 64 位)';
|
||||
static const uploadQR = '上传二维码';
|
||||
static const login = '登录';
|
||||
@@ -124,33 +123,29 @@ class AppStrings {
|
||||
static const machineSettings = '机器信息';
|
||||
static const save = '保存';
|
||||
static const labelTitleServerUrl = 'Title Server URL';
|
||||
static const hintTitleServerUrl = 'https://maimai-gm.wahlap.com:42081/Maimai2Servlet/';
|
||||
static const labelAesKey = 'AES Key';
|
||||
static const hintAesKey = 'your aes key string';
|
||||
static const labelAesIv = 'AES IV';
|
||||
static const hintAesIv = 'your aes iv string';
|
||||
static const labelClientId = 'Client ID';
|
||||
static const hintClientId = 'A63E01C2805';
|
||||
static const labelRegionId = 'Region ID';
|
||||
static const hintRegionId = '1';
|
||||
static const labelPlaceId = 'Place ID';
|
||||
static const hintPlaceId = '1403';
|
||||
static const labelObfuscateParam = 'Obfuscate Param';
|
||||
static const hintObfuscateParam = 'LatuAa81';
|
||||
static const labelApiVersion = 'API Version (Mai-Encoding)';
|
||||
static const hintApiVersion = '1.53';
|
||||
static const labelRegionName = 'Region 名称';
|
||||
static const hintRegionName = '北京';
|
||||
static const labelPlaceName = 'Place 名称';
|
||||
static const hintPlaceName = '插电师北京王府井银泰店';
|
||||
static const labelKeychipId = 'Keychip ID';
|
||||
static const hintKeychipId = 'A63E-01C28055905';
|
||||
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 hintAimeSalt = 'API Key 派生用 Salt';
|
||||
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
|
||||
static const aboutTitle = 'Project Ichino';
|
||||
static const aboutDesc = '基于 QR Code 的 maimai DX 街机网络登录客户端。';
|
||||
|
||||
@@ -22,18 +22,18 @@ class TitleServerConfig {
|
||||
required this.titleServerUrl,
|
||||
required this.aesKey,
|
||||
required this.aesIv,
|
||||
this.obfuscateParam = 'LatuAa81',
|
||||
this.apiVersion = '1.53',
|
||||
this.obfuscateParam = '',
|
||||
this.apiVersion = '',
|
||||
required this.clientId,
|
||||
this.regionId = 1,
|
||||
this.placeId = 1403,
|
||||
this.regionId = 0,
|
||||
this.placeId = 0,
|
||||
this.regionName = '',
|
||||
this.placeName = '',
|
||||
this.keychipId = '',
|
||||
this.aimeUrl = '',
|
||||
this.aimeSalt = '',
|
||||
String? openGameID,
|
||||
}) : openGameID = openGameID ?? 'MAID';
|
||||
}) : openGameID = openGameID ?? '';
|
||||
|
||||
List<int> get aesKeyBytes => utf8.encode(aesKey);
|
||||
List<int> get aesIvBytes => utf8.encode(aesIv);
|
||||
@@ -60,17 +60,17 @@ class TitleServerConfig {
|
||||
titleServerUrl: json['titleServerUrl'] as String? ?? '',
|
||||
aesKey: json['aesKey'] as String? ?? '',
|
||||
aesIv: json['aesIv'] as String? ?? '',
|
||||
obfuscateParam: json['obfuscateParam'] as String? ?? 'LatuAa81',
|
||||
apiVersion: json['apiVersion'] as String? ?? '1.53',
|
||||
obfuscateParam: json['obfuscateParam'] as String? ?? '',
|
||||
apiVersion: json['apiVersion'] as String? ?? '',
|
||||
clientId: json['clientId'] as String? ?? '',
|
||||
regionId: json['regionId'] as int? ?? 1,
|
||||
placeId: json['placeId'] as int? ?? 1403,
|
||||
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? ?? '',
|
||||
aimeUrl: json['aimeUrl'] as String? ?? '',
|
||||
aimeSalt: json['aimeSalt'] as String? ?? '',
|
||||
openGameID: json['openGameID'] as String? ?? 'MAID',
|
||||
openGameID: json['openGameID'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,12 +227,6 @@ class _LoginPageState extends State<LoginPage> {
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: AppStrings.qrHint,
|
||||
hintStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant
|
||||
.withValues(alpha: 0.5),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../config/strings.dart';
|
||||
import '../config/title_server_config.dart';
|
||||
import 'config_export_import_page.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
final bool showAppBar;
|
||||
@@ -18,10 +19,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
final _aesKeyController = TextEditingController();
|
||||
final _aesIvController = TextEditingController();
|
||||
final _clientIdController = TextEditingController();
|
||||
final _regionIdController = TextEditingController(text: '1');
|
||||
final _placeIdController = TextEditingController(text: '1403');
|
||||
final _obfuscateController = TextEditingController(text: 'LatuAa81');
|
||||
final _apiVersionController = TextEditingController(text: '1.53');
|
||||
final _regionIdController = TextEditingController();
|
||||
final _placeIdController = TextEditingController();
|
||||
final _obfuscateController = TextEditingController();
|
||||
final _apiVersionController = TextEditingController();
|
||||
final _regionNameController = TextEditingController();
|
||||
final _placeNameController = TextEditingController();
|
||||
final _keychipIdController = TextEditingController();
|
||||
@@ -75,8 +76,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
aesKey: _aesKeyController.text.trim(),
|
||||
aesIv: _aesIvController.text.trim(),
|
||||
clientId: _clientIdController.text.trim(),
|
||||
regionId: int.tryParse(_regionIdController.text.trim()) ?? 1,
|
||||
placeId: int.tryParse(_placeIdController.text.trim()) ?? 1403,
|
||||
regionId: int.tryParse(_regionIdController.text.trim()) ?? 0,
|
||||
placeId: int.tryParse(_placeIdController.text.trim()) ?? 0,
|
||||
obfuscateParam: _obfuscateController.text.trim(),
|
||||
apiVersion: _apiVersionController.text.trim(),
|
||||
regionName: _regionNameController.text.trim(),
|
||||
@@ -109,35 +110,35 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildSection(theme, AppStrings.titleServerSection, [
|
||||
_buildField(controller: _urlController, label: AppStrings.labelTitleServerUrl, hint: AppStrings.hintTitleServerUrl),
|
||||
_buildField(controller: _urlController, label: AppStrings.labelTitleServerUrl),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _aesKeyController, label: AppStrings.labelAesKey, hint: AppStrings.hintAesKey),
|
||||
_buildField(controller: _aesKeyController, label: AppStrings.labelAesKey),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _aesIvController, label: AppStrings.labelAesIv, hint: AppStrings.hintAesIv),
|
||||
_buildField(controller: _aesIvController, label: AppStrings.labelAesIv),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _clientIdController, label: AppStrings.labelClientId, hint: AppStrings.hintClientId),
|
||||
_buildField(controller: _clientIdController, label: AppStrings.labelClientId),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _obfuscateController, label: AppStrings.labelObfuscateParam, hint: AppStrings.hintObfuscateParam),
|
||||
_buildField(controller: _obfuscateController, label: AppStrings.labelObfuscateParam),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _apiVersionController, label: AppStrings.labelApiVersion, hint: AppStrings.hintApiVersion),
|
||||
_buildField(controller: _apiVersionController, label: AppStrings.labelApiVersion),
|
||||
]),
|
||||
const SizedBox(height: 24),
|
||||
_buildSection(theme, AppStrings.machineSettings, [
|
||||
_buildField(controller: _regionIdController, label: AppStrings.labelRegionId, hint: AppStrings.hintRegionId),
|
||||
_buildField(controller: _regionIdController, label: AppStrings.labelRegionId),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _regionNameController, label: AppStrings.labelRegionName, hint: AppStrings.hintRegionName),
|
||||
_buildField(controller: _regionNameController, label: AppStrings.labelRegionName),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _placeIdController, label: AppStrings.labelPlaceId, hint: AppStrings.hintPlaceId),
|
||||
_buildField(controller: _placeIdController, label: AppStrings.labelPlaceId),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _placeNameController, label: AppStrings.labelPlaceName, hint: AppStrings.hintPlaceName),
|
||||
_buildField(controller: _placeNameController, label: AppStrings.labelPlaceName),
|
||||
]),
|
||||
const SizedBox(height: 24),
|
||||
_buildSection(theme, AppStrings.authServerSettings, [
|
||||
_buildField(controller: _keychipIdController, label: AppStrings.labelKeychipId, hint: AppStrings.hintKeychipId),
|
||||
_buildField(controller: _keychipIdController, label: AppStrings.labelKeychipId),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _aimeUrlController, label: AppStrings.labelAimeUrl, hint: AppStrings.hintAimeUrl),
|
||||
_buildField(controller: _aimeUrlController, label: AppStrings.labelAimeUrl),
|
||||
const SizedBox(height: 14),
|
||||
_buildField(controller: _aimeSaltController, label: AppStrings.labelAimeSalt, hint: AppStrings.hintAimeSalt),
|
||||
_buildField(controller: _aimeSaltController, label: AppStrings.labelAimeSalt),
|
||||
]),
|
||||
const SizedBox(height: 32),
|
||||
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({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String hint,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user