From cd6491cdc5ac6cfba709da0661f5ecfd7066b794 Mon Sep 17 00:00:00 2001 From: chuxuehaocai Date: Fri, 29 May 2026 16:21:54 +0800 Subject: [PATCH] feat.Hatsune Miku --- lib/config/strings.dart | 23 ++--- lib/config/title_server_config.dart | 20 ++-- lib/main.dart | 6 -- lib/pages/config_export_import_page.dart | 111 +++++++++++++++++++++++ lib/pages/settings_page.dart | 57 +++++++----- 5 files changed, 166 insertions(+), 51 deletions(-) create mode 100644 lib/pages/config_export_import_page.dart diff --git a/lib/config/strings.dart b/lib/config/strings.dart index 32283c3..6bdbe55 100644 --- a/lib/config/strings.dart +++ b/lib/config/strings.dart @@ -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 街机网络登录客户端。'; diff --git a/lib/config/title_server_config.dart b/lib/config/title_server_config.dart index e378f80..d4001df 100644 --- a/lib/config/title_server_config.dart +++ b/lib/config/title_server_config.dart @@ -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 get aesKeyBytes => utf8.encode(aesKey); List 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? ?? '', ); } } diff --git a/lib/main.dart b/lib/main.dart index 8df26f0..c0e8050 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -227,12 +227,6 @@ class _LoginPageState extends State { 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), ), diff --git a/lib/pages/config_export_import_page.dart b/lib/pages/config_export_import_page.dart new file mode 100644 index 0000000..1a15bc0 --- /dev/null +++ b/lib/pages/config_export_import_page.dart @@ -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 _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 _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; + 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), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 7b8f3fe..98dc166 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -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 { 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 { 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 { 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 { ), ), ), + 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 { 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), ),