diff --git a/lib/config/responsive.dart b/lib/config/responsive.dart new file mode 100644 index 0000000..4dbc6bf --- /dev/null +++ b/lib/config/responsive.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +/// Breakpoints: phone < 600, tablet 600–900, desktop > 900 +const double _breakpointTablet = 600; +const double _breakpointDesktop = 900; + +/// Returns a responsive max width for content containers. +/// - Phone: 500 +/// - Tablet: 700 +/// - Desktop: 900 +double responsiveMaxWidth(BuildContext context) { + final width = MediaQuery.of(context).size.width; + if (width >= _breakpointDesktop) return 900; + if (width >= _breakpointTablet) return 700; + return 500; +} + +/// Whether the screen is tablet-sized or wider. +bool isTablet(BuildContext context) => + MediaQuery.of(context).size.width >= _breakpointTablet; + +/// Whether the screen is desktop-sized or wider. +bool isDesktop(BuildContext context) => + MediaQuery.of(context).size.width >= _breakpointDesktop; + +/// Build columns for tablet/desktop — on phone, each item is full-width; +/// on tablet+ they are laid out in [columns] columns. +List responsiveGrid({ + required BuildContext context, + required List children, + int columns = 2, + double spacing = 12, +}) { + final tablet = isTablet(context); + if (!tablet) return children; + + final rows = []; + final padded = []; + for (int i = 0; i < children.length; i++) { + padded.add(children[i]); + if ((i + 1) % columns == 0 || i == children.length - 1) { + final cols = []; + for (int j = 0; j < padded.length; j++) { + cols.add(Expanded(child: padded[j])); + } + // pad incomplete last row with empty expanded widgets + while (cols.length < columns) { + cols.add(const Expanded(child: SizedBox.shrink())); + } + rows.add(Padding( + padding: EdgeInsets.only(bottom: rows.isEmpty ? 0 : spacing), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int j = 0; j < cols.length; j++) ...[ + if (j > 0) SizedBox(width: spacing), + cols[j], + ], + ], + ), + )); + padded.clear(); + } + } + return rows; +} diff --git a/lib/config/strings.dart b/lib/config/strings.dart index 6bdbe55..0b33eeb 100644 --- a/lib/config/strings.dart +++ b/lib/config/strings.dart @@ -11,6 +11,7 @@ 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 = '登录'; @@ -123,18 +124,31 @@ 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 = 'Place name'; static const labelKeychipId = 'Keychip ID'; + static const hintKeychipId = 'ID'; 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 diff --git a/lib/config/title_server_config.dart b/lib/config/title_server_config.dart index d4001df..684f8c9 100644 --- a/lib/config/title_server_config.dart +++ b/lib/config/title_server_config.dart @@ -33,7 +33,7 @@ class TitleServerConfig { this.aimeUrl = '', this.aimeSalt = '', String? openGameID, - }) : openGameID = openGameID ?? ''; + }) : openGameID = openGameID ?? 'MAID'; List get aesKeyBytes => utf8.encode(aesKey); List get aesIvBytes => utf8.encode(aesIv); @@ -70,7 +70,7 @@ class TitleServerConfig { keychipId: json['keychipId'] as String? ?? '', aimeUrl: json['aimeUrl'] as String? ?? '', aimeSalt: json['aimeSalt'] as String? ?? '', - openGameID: json['openGameID'] as String? ?? '', + openGameID: json['openGameID'] as String? ?? 'MAID', ); } } diff --git a/lib/main.dart b/lib/main.dart index c0e8050..891de95 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'config/responsive.dart'; import 'config/strings.dart'; import 'config/title_server_config.dart'; import 'services/api_service.dart'; @@ -161,7 +162,7 @@ class _LoginPageState extends State { child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 440), + constraints: BoxConstraints(maxWidth: responsiveMaxWidth(context)), child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/pages/about_page.dart b/lib/pages/about_page.dart index cfbbfdf..4dafd9c 100644 --- a/lib/pages/about_page.dart +++ b/lib/pages/about_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../config/responsive.dart'; import '../config/strings.dart'; class AboutPage extends StatelessWidget { @@ -12,7 +13,7 @@ class AboutPage extends StatelessWidget { return SingleChildScrollView( padding: const EdgeInsets.all(24), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), + constraints: BoxConstraints(maxWidth: responsiveMaxWidth(context)), child: Column( children: [ const SizedBox(height: 32), diff --git a/lib/pages/config_export_import_page.dart b/lib/pages/config_export_import_page.dart index 1a15bc0..136166e 100644 --- a/lib/pages/config_export_import_page.dart +++ b/lib/pages/config_export_import_page.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../config/responsive.dart'; import '../config/strings.dart'; import '../config/title_server_config.dart'; @@ -73,7 +74,7 @@ class ConfigExportImportPage extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(24), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), + constraints: BoxConstraints(maxWidth: responsiveMaxWidth(context)), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index f6d52ab..c3d754a 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../config/responsive.dart'; import '../config/strings.dart'; import '../config/title_server_config.dart'; import '../models/user_data.dart'; @@ -343,7 +344,7 @@ class _HomePageState extends State { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), + constraints: BoxConstraints(maxWidth: responsiveMaxWidth(context)), child: Column( children: [ if (preview.errorId != 0) _ErrorIdBanner(theme: theme, errorId: preview.errorId), @@ -387,9 +388,13 @@ class _HomePageState extends State { const SizedBox(height: 12), _ProfileFromPreviewCard(theme: theme, preview: preview), const SizedBox(height: 12), - _PreviewGameInfoCard(theme: theme, preview: preview), - const SizedBox(height: 12), - _PreviewStatusCard(theme: theme, preview: preview), + ...responsiveGrid( + context: context, + children: [ + _PreviewGameInfoCard(theme: theme, preview: preview), + _PreviewStatusCard(theme: theme, preview: preview), + ], + ), if (TitleApiService.lastRawResponse != null) ...[ const SizedBox(height: 12), _DebugRawJsonCard( @@ -411,22 +416,22 @@ class _HomePageState extends State { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), + constraints: BoxConstraints(maxWidth: responsiveMaxWidth(context)), child: Column( children: [ _ProfileFromUserDataCard(theme: theme, data: data), const SizedBox(height: 12), - _RatingBreakdownCard(theme: theme, data: data), - const SizedBox(height: 12), - _FirstPlayCard(theme: theme, data: data), - const SizedBox(height: 12), - _GameInfoFromUserDataCard(theme: theme, data: data), - const SizedBox(height: 12), - _PlayStatsCard(theme: theme, data: data), - const SizedBox(height: 12), - _StatusFromUserDataCard(theme: theme, preview: preview, data: data), - const SizedBox(height: 12), - _UserDataDetailCard(theme: theme, data: data), + ...responsiveGrid( + context: context, + children: [ + _RatingBreakdownCard(theme: theme, data: data), + _FirstPlayCard(theme: theme, data: data), + _GameInfoFromUserDataCard(theme: theme, data: data), + _PlayStatsCard(theme: theme, data: data), + _StatusFromUserDataCard(theme: theme, preview: preview, data: data), + _UserDataDetailCard(theme: theme, data: data), + ], + ), if (TitleApiService.lastRawResponse != null) ...[ const SizedBox(height: 12), _DebugRawJsonCard( diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 98dc166..03c7434 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../config/responsive.dart'; import '../config/strings.dart'; import '../config/title_server_config.dart'; import 'config_export_import_page.dart'; @@ -103,7 +104,7 @@ class _SettingsPageState extends State { body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), + constraints: BoxConstraints(maxWidth: responsiveMaxWidth(context)), child: Form( key: _formKey, child: Column( diff --git a/lib/pages/ticket_page.dart b/lib/pages/ticket_page.dart index d374807..c8ed29f 100644 --- a/lib/pages/ticket_page.dart +++ b/lib/pages/ticket_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import '../config/responsive.dart'; import '../config/strings.dart'; import '../config/title_server_config.dart'; import '../services/title_api_service.dart'; @@ -251,7 +252,7 @@ class _TicketPageState extends State { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), + constraints: BoxConstraints(maxWidth: responsiveMaxWidth(context)), child: Column( children: [ if (widget.loginDateTime == null) _buildNotLoggedInBanner(theme), diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 368a13c..7fe6431 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -27,19 +27,13 @@ class ApiService { ApiService([this._config]); TitleServerConfig? get _cfg => _config ?? TitleServerConfigHolder().config; - String get _chipId { - final v = _cfg?.keychipId; - return (v != null && v.isNotEmpty) ? v : 'A63E-01C28055905'; + String get _chipId => _cfg?.keychipId ?? ''; + String get _aimeSalt => _cfg?.aimeSalt ?? ''; + String get _aimeUrl => _cfg?.aimeUrl ?? ''; + String get _openGameID { + final v = _cfg?.openGameID; + return (v != null && v.isNotEmpty) ? v : 'MAID'; } - String get _aimeSalt { - final v = _cfg?.aimeSalt; - return (v != null && v.isNotEmpty) ? v : 'XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW'; - } - String get _aimeUrl { - final v = _cfg?.aimeUrl; - return (v != null && v.isNotEmpty) ? v : 'http://ai.sys-allnet.cn/wc_aime/api/get_data'; - } - String get _openGameID => _cfg?.openGameID ?? 'MAID'; String _formatTimestamp() { final tokyo = DateTime.now().toUtc().add(const Duration(hours: 9));