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,243 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../config/strings.dart';
|
||||
import '../app_card.dart';
|
||||
|
||||
/// 主页头像:iconId 为 0 时显示占位人形。
|
||||
class HomeAvatar extends StatelessWidget {
|
||||
final int iconId;
|
||||
|
||||
const HomeAvatar({super.key, required this.iconId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
backgroundImage: iconId > 0
|
||||
? NetworkImage('https://assets2.lxns.net/maimai/icon/$iconId.png')
|
||||
: null,
|
||||
child: iconId == 0
|
||||
? Icon(Icons.person, color: theme.colorScheme.onPrimaryContainer)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 主页顶部的账号概要卡,Preview 与完整数据共用。
|
||||
///
|
||||
/// [highestRating] 为 null 时不显示「历史最高」一行(Preview 数据里没有)。
|
||||
class HomeProfileCard extends StatelessWidget {
|
||||
final String userName;
|
||||
final int iconId;
|
||||
final int playerRating;
|
||||
final String? highestRating;
|
||||
|
||||
const HomeProfileCard({
|
||||
super.key,
|
||||
required this.userName,
|
||||
required this.iconId,
|
||||
required this.playerRating,
|
||||
this.highestRating,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return AppCard(
|
||||
child: Row(
|
||||
children: [
|
||||
HomeAvatar(iconId: iconId),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
userName.isEmpty ? AppStrings.unknownUser : userName,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
AppStrings.rating(playerRating),
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (highestRating != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${AppStrings.highestRating}: $highestRating',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 主页的「标题 + 字段行」信息卡。
|
||||
class HomeSectionCard extends StatelessWidget {
|
||||
final String title;
|
||||
final List<(String, String)> rows;
|
||||
|
||||
const HomeSectionCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.rows,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return AppCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
for (final (label, value) in rows)
|
||||
_InfoRow(label: label, value: value),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value.isEmpty ? '-' : value,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 主页状态卡:标题 + 若干 [HomeStatusChip]。
|
||||
class HomeStatusCard extends StatelessWidget {
|
||||
final List<Widget> chips;
|
||||
|
||||
const HomeStatusCard({super.key, required this.chips});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return AppCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppStrings.status,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
...chips,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态行:bool 按成功/失败色渲染成胶囊,其他值原样显示。
|
||||
class HomeStatusChip extends StatelessWidget {
|
||||
final String label;
|
||||
final Object? value;
|
||||
final String trueText;
|
||||
final String falseText;
|
||||
|
||||
const HomeStatusChip(
|
||||
this.label,
|
||||
this.value, {
|
||||
super.key,
|
||||
this.trueText = 'Yes',
|
||||
this.falseText = 'No',
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final v = value;
|
||||
final String text;
|
||||
final Color color;
|
||||
if (v is bool) {
|
||||
text = v ? trueText : falseText;
|
||||
color = v ? Colors.green : Colors.grey;
|
||||
} else {
|
||||
text = '$v';
|
||||
color = theme.colorScheme.onSurface;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textTheme.labelSmall?.copyWith(color: color),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../config/strings.dart';
|
||||
import '../app_card.dart';
|
||||
|
||||
/// 调试用的原始 API 响应卡,默认折叠。
|
||||
class DebugRawJsonCard extends StatefulWidget {
|
||||
final String rawJson;
|
||||
|
||||
const DebugRawJsonCard({super.key, required this.rawJson});
|
||||
|
||||
@override
|
||||
State<DebugRawJsonCard> createState() => _DebugRawJsonCardState();
|
||||
}
|
||||
|
||||
class _DebugRawJsonCardState extends State<DebugRawJsonCard> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AppCard(
|
||||
borderColor: Colors.orange.withValues(alpha: 0.5),
|
||||
onTap: () => setState(() => _expanded = !_expanded),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.bug_report, size: 16, color: Colors.orange),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppStrings.rawApiResponse,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
_expanded ? Icons.expand_less : Icons.expand_more,
|
||||
color: Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_expanded) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(
|
||||
widget.rawJson,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorIdBanner extends StatelessWidget {
|
||||
final int errorId;
|
||||
|
||||
const ErrorIdBanner({super.key, required this.errorId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
size: 20,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'API returned errorId: $errorId',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../config/strings.dart';
|
||||
import '../../models/user_preview.dart';
|
||||
import 'home_common.dart';
|
||||
|
||||
class PreviewGameInfoCard extends StatelessWidget {
|
||||
final UserPreviewDataBean preview;
|
||||
|
||||
const PreviewGameInfoCard({super.key, required this.preview});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HomeSectionCard(
|
||||
title: AppStrings.gameInfo,
|
||||
rows: [
|
||||
(AppStrings.lastGame, preview.lastGameId),
|
||||
(AppStrings.lastPlay, preview.lastPlayDate),
|
||||
(AppStrings.lastLogin, preview.lastLoginDate),
|
||||
(AppStrings.romVersion, preview.lastRomVersion),
|
||||
(AppStrings.dataVersion, preview.lastDataVersion),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PreviewStatusCard extends StatelessWidget {
|
||||
final UserPreviewDataBean preview;
|
||||
|
||||
const PreviewStatusCard({super.key, required this.preview});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HomeStatusCard(
|
||||
chips: [
|
||||
HomeStatusChip(AppStrings.netMember, preview.isNetMember),
|
||||
HomeStatusChip(AppStrings.inherit, preview.isInherit),
|
||||
HomeStatusChip(
|
||||
AppStrings.banState,
|
||||
preview.banState == 0,
|
||||
trueText: AppStrings.clean,
|
||||
falseText: '已封禁 (${preview.banState})',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../config/strings.dart';
|
||||
import '../../models/user_data.dart';
|
||||
import '../../models/user_preview.dart';
|
||||
import '../app_card.dart';
|
||||
import 'home_common.dart';
|
||||
|
||||
class RatingBreakdownCard extends StatelessWidget {
|
||||
final UserDataBean data;
|
||||
|
||||
const RatingBreakdownCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HomeSectionCard(
|
||||
title: AppStrings.ratingBreakdown,
|
||||
rows: [
|
||||
(AppStrings.musicRating, '${data.musicRating}'),
|
||||
(AppStrings.newChartRating, '${data.playerNewRating}'),
|
||||
(AppStrings.oldChartRating, '${data.playerOldRating}'),
|
||||
(AppStrings.highestRating, '${data.highestRating}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FirstPlayCard extends StatelessWidget {
|
||||
final UserDataBean data;
|
||||
|
||||
const FirstPlayCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HomeSectionCard(
|
||||
title: AppStrings.firstPlay,
|
||||
rows: [
|
||||
(AppStrings.joinDate, data.firstPlayDate),
|
||||
(AppStrings.firstGame, data.firstGameId),
|
||||
(AppStrings.firstRomVersion, data.firstRomVersion),
|
||||
(AppStrings.firstDataVersion, data.firstDataVersion),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GameInfoFromUserDataCard extends StatelessWidget {
|
||||
final UserDataBean data;
|
||||
|
||||
const GameInfoFromUserDataCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HomeSectionCard(
|
||||
title: AppStrings.gameInfo,
|
||||
rows: [
|
||||
(AppStrings.lastGame, data.lastGameId),
|
||||
(AppStrings.lastPlay, data.lastPlayDate),
|
||||
(AppStrings.lastLogin, data.lastLoginDate),
|
||||
(AppStrings.romVersion, data.lastRomVersion),
|
||||
(AppStrings.dataVersion, data.lastDataVersion),
|
||||
(
|
||||
AppStrings.lastRegion,
|
||||
'${data.lastRegionName}${data.lastRegionId > 0 ? ' (${data.lastRegionId})' : ''}',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlayStatsCard extends StatelessWidget {
|
||||
final UserDataBean data;
|
||||
|
||||
const PlayStatsCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HomeSectionCard(
|
||||
title: AppStrings.playStats,
|
||||
rows: [
|
||||
(AppStrings.playCount, '${data.playCount}'),
|
||||
(AppStrings.currentPlayCount, '${data.currentPlayCount}'),
|
||||
(AppStrings.totalAchievement, '${data.totalAchievement}'),
|
||||
(AppStrings.totalDxScore, '${data.totalDeluxscore}'),
|
||||
(AppStrings.totalSync, '${data.totalSync}'),
|
||||
(AppStrings.totalAwake, '${data.totalAwake}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusFromUserDataCard extends StatelessWidget {
|
||||
/// 用连接信息(仅 JSESSIONID)恢复的会话没有走 GetUserPreviewApi,可为空。
|
||||
final UserPreviewDataBean? preview;
|
||||
final UserDataBean data;
|
||||
|
||||
const StatusFromUserDataCard({
|
||||
super.key,
|
||||
required this.preview,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = preview;
|
||||
|
||||
return HomeStatusCard(
|
||||
chips: [
|
||||
HomeStatusChip(AppStrings.netMember, data.isNetMember),
|
||||
if (p != null) HomeStatusChip(AppStrings.inherit, p.isInherit),
|
||||
HomeStatusChip(
|
||||
AppStrings.banState,
|
||||
data.banState == 0,
|
||||
trueText: AppStrings.clean,
|
||||
falseText: '已封禁 (${data.banState})',
|
||||
),
|
||||
HomeStatusChip(
|
||||
AppStrings.dailyBonus,
|
||||
data.dailyBonusDate.isNotEmpty &&
|
||||
!data.dailyBonusDate.startsWith('1970'),
|
||||
trueText: data.dailyBonusDate,
|
||||
falseText: AppStrings.notClaimed,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 可展开的原始字段卡。
|
||||
class UserDataDetailCard extends StatefulWidget {
|
||||
final UserDataBean data;
|
||||
|
||||
const UserDataDetailCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
State<UserDataDetailCard> createState() => _UserDataDetailCardState();
|
||||
}
|
||||
|
||||
class _UserDataDetailCardState extends State<UserDataDetailCard> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final data = widget.data;
|
||||
|
||||
return AppCard(
|
||||
onTap: () => setState(() => _expanded = !_expanded),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppStrings.moreDetails,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
_expanded ? Icons.expand_less : Icons.expand_more,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_expanded) ...[
|
||||
const SizedBox(height: 14),
|
||||
_detailRow(theme, AppStrings.userId, '${data.userId}'),
|
||||
_detailRow(theme, AppStrings.point, '${data.point}'),
|
||||
_detailRow(theme, AppStrings.totalPoint, '${data.totalPoint}'),
|
||||
_detailRow(theme, AppStrings.iconId, '${data.iconId}'),
|
||||
_detailRow(theme, AppStrings.nameplateId, '${data.nameplateId}'),
|
||||
_detailRow(theme, AppStrings.plateId, '${data.plateId}'),
|
||||
_detailRow(theme, AppStrings.frameId, '${data.frameId}'),
|
||||
_detailRow(theme, AppStrings.titleId, '${data.titleId}'),
|
||||
_detailRow(theme, AppStrings.trophyId, '${data.trophyId}'),
|
||||
_detailRow(theme, AppStrings.partnerId, '${data.partnerId}'),
|
||||
_detailRow(theme, AppStrings.gradeRank, '${data.gradeRank}'),
|
||||
_detailRow(theme, AppStrings.courseRank, '${data.courseRank}'),
|
||||
_detailRow(theme, AppStrings.classRank, '${data.classRank}'),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _detailRow(ThemeData theme, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value, style: theme.textTheme.bodyMedium)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user