refactor: 拆分主页与公共组件,扫码登录改走 WinHTTP
- 抽出 lib/core 的日期/JSON 工具,收敛各页重复解析 - 主页与音乐风险页拆成组件,风险功能共用一套运行框架 - 修复设置页作为主页页签保存时弹掉根路由导致的黑屏 - 登录对齐 Astral aimedb 协议:84 位二维码校验、签名与请求头修正, 补 GlobalSign 中间证书;Windows 改用 WinHTTP 绕过服务端对 BoringSSL 的 TLS 指纹拦截(证书校验保持开启) - 补开发预览数据与测试
This commit is contained in:
1 parent
0cb5320290
commit
e9357f5969
55 files changed
+4387
-3392
No files matched your search
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../config/strings.dart';
|
||||
|
||||
/// 主页底部导航的六个页签,顺序即 [IndexedStack] 的下标。
|
||||
enum HomeTab {
|
||||
home(AppStrings.tabHome, Icons.home_outlined, Icons.home),
|
||||
tickets(
|
||||
AppStrings.tabTickets,
|
||||
Icons.confirmation_number_outlined,
|
||||
Icons.confirmation_number,
|
||||
),
|
||||
best50(AppStrings.tabBest50, Icons.bar_chart_outlined, Icons.bar_chart),
|
||||
risk(AppStrings.tabRisk, Icons.warning_outlined, Icons.inventory_2),
|
||||
settings(AppStrings.tabSettings, Icons.settings_outlined, Icons.settings),
|
||||
about(AppStrings.tabAbout, Icons.info_outlined, Icons.info);
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
|
||||
const HomeTab(this.title, this.icon, this.selectedIcon);
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../config/responsive.dart';
|
||||
import '../../config/strings.dart';
|
||||
import '../../config/title_server_config.dart';
|
||||
import '../../models/user_data.dart';
|
||||
import '../../models/user_preview.dart';
|
||||
import '../../widgets/account_backup_card.dart';
|
||||
import '../../widgets/app_notice.dart';
|
||||
import '../../widgets/connection_share_card.dart';
|
||||
import '../../widgets/home/home_common.dart';
|
||||
import '../../widgets/home/home_debug_card.dart';
|
||||
import '../../widgets/home/home_preview_cards.dart';
|
||||
import '../../widgets/home/home_user_data_cards.dart';
|
||||
|
||||
/// 主页第一个页签的内容:账号概要、统计卡与调试信息。
|
||||
///
|
||||
/// 数据与回调都由 [HomePage] 持有,这里只负责按加载状态切换展示。
|
||||
class HomeTabView extends StatelessWidget {
|
||||
final int userId;
|
||||
final String token;
|
||||
final String? cookies;
|
||||
final UserPreviewDataBean? preview;
|
||||
final UserDataBean? userData;
|
||||
final bool loading;
|
||||
final String? error;
|
||||
final bool isGameLoggedIn;
|
||||
final bool forcePreviewApi;
|
||||
final bool showRestoredNotice;
|
||||
final String? rawJson;
|
||||
|
||||
final VoidCallback onRetry;
|
||||
final VoidCallback onOpenSettings;
|
||||
final VoidCallback onLogin;
|
||||
|
||||
const HomeTabView({
|
||||
super.key,
|
||||
required this.userId,
|
||||
required this.token,
|
||||
required this.cookies,
|
||||
required this.preview,
|
||||
required this.userData,
|
||||
required this.loading,
|
||||
required this.error,
|
||||
required this.isGameLoggedIn,
|
||||
required this.forcePreviewApi,
|
||||
required this.showRestoredNotice,
|
||||
required this.rawJson,
|
||||
required this.onRetry,
|
||||
required this.onOpenSettings,
|
||||
required this.onLogin,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// 开发预览等入口会直接塞入种子数据,此时没配 Title Server 也应该展示。
|
||||
if (!TitleServerConfigHolder().isConfigured &&
|
||||
userData == null &&
|
||||
preview == null) {
|
||||
return _ConfigMissingView(onOpenSettings: onOpenSettings);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
return _ErrorView(theme: theme, error: error!, onRetry: onRetry);
|
||||
}
|
||||
|
||||
final data = userData;
|
||||
if (data != null) {
|
||||
return _buildFullData(context, data);
|
||||
}
|
||||
|
||||
final p = preview;
|
||||
if (p == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final showLoginButton = forcePreviewApi && !isGameLoggedIn;
|
||||
if (p.isLogin || showLoginButton) {
|
||||
return _buildPreviewOnly(
|
||||
context,
|
||||
p,
|
||||
showLoginButton: showLoginButton,
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildPreviewOnly(
|
||||
BuildContext context,
|
||||
UserPreviewDataBean preview, {
|
||||
required bool showLoginButton,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: responsiveBody(
|
||||
context,
|
||||
child: Column(
|
||||
children: [
|
||||
if (preview.errorId != 0) ErrorIdBanner(errorId: preview.errorId),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.tertiaryContainer.withValues(
|
||||
alpha: 0.4,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
showLoginButton
|
||||
? 'Preview API 模式,点击下方按钮登录。'
|
||||
: AppStrings.inheritedAccountNotice,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showLoginButton) ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: onLogin,
|
||||
icon: const Icon(Icons.login, size: 20),
|
||||
label: const Text(AppStrings.login),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
HomeProfileCard(
|
||||
userName: preview.userName,
|
||||
iconId: preview.iconId,
|
||||
playerRating: preview.playerRating,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...responsiveGrid(
|
||||
context: context,
|
||||
children: [
|
||||
PreviewGameInfoCard(preview: preview),
|
||||
PreviewStatusCard(preview: preview),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ConnectionShareCard(userId: userId, token: token),
|
||||
if (rawJson != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
DebugRawJsonCard(rawJson: rawJson!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFullData(BuildContext context, UserDataBean data) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: responsiveBody(
|
||||
context,
|
||||
child: Column(
|
||||
children: [
|
||||
if (showRestoredNotice)
|
||||
AppNotice(
|
||||
AppStrings.sessionRestoredNotice,
|
||||
icon: Icons.science_outlined,
|
||||
),
|
||||
HomeProfileCard(
|
||||
userName: data.userName,
|
||||
iconId: data.iconId,
|
||||
playerRating: data.playerRating,
|
||||
highestRating: '${data.highestRating}',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...responsiveGrid(
|
||||
context: context,
|
||||
children: [
|
||||
RatingBreakdownCard(data: data),
|
||||
FirstPlayCard(data: data),
|
||||
GameInfoFromUserDataCard(data: data),
|
||||
PlayStatsCard(data: data),
|
||||
StatusFromUserDataCard(preview: preview, data: data),
|
||||
UserDataDetailCard(data: data),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ConnectionShareCard(userId: userId, token: token),
|
||||
const SizedBox(height: 12),
|
||||
AccountBackupCard(userId: userId, cookies: cookies),
|
||||
if (rawJson != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
DebugRawJsonCard(rawJson: rawJson!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConfigMissingView extends StatelessWidget {
|
||||
final VoidCallback onOpenSettings;
|
||||
|
||||
const _ConfigMissingView({required this.onOpenSettings});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_ethernet,
|
||||
size: 64,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
AppStrings.titleServerNotConfigured,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
AppStrings.titleServerNotConfiguredDesc,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: onOpenSettings,
|
||||
icon: const Icon(Icons.settings, size: 20),
|
||||
label: const Text(AppStrings.openSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorView extends StatelessWidget {
|
||||
final ThemeData theme;
|
||||
final String error;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
const _ErrorView({
|
||||
required this.theme,
|
||||
required this.error,
|
||||
required this.onRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(AppStrings.loadFailed, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh, size: 20),
|
||||
label: const Text(AppStrings.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user