feat: 实验性连接信息导出与会话恢复,重写关于页
- SessionShare:把 userId / JSESSIONID / token 打成 Base64(JSON),Cookie 只保留 JSESSIONID 一项,粘贴时容忍换行与空白,QR 令牌与乱码解不出来 - 主页底部新增「导出连接信息」卡片,标记实验性:内容未加密,字符串在手即可操作账号 - 登录页识别到连接信息时把登录按钮换成「恢复会话」,不必再扫 QR 令牌 - 恢复优先沿用导入的 JSESSIONID(不重发 UserLoginApi,免得顶掉机台上的登录), Cookie 失效才退回用 token 登录;验证 Cookie 时顺手拉到的 GetUserDataApi 结果 直接交给主页展示,省一次请求 - 仅凭 Cookie 恢复的会话没有 loginId / loginDateTime,_gameLogin 保持 null, 票据与风险页照旧提示「尚未登录游戏服务器」,主页顶部给出说明横幅 - 关于页改用 AppCard / SectionTitle 脚手架,补基本信息与风险提示,把过时的 「jsQR (web)」致谢换成原生 MethodChannel(QR 解码早已不走 web 库)
This commit is contained in:
1 parent
4dddfdc00c
commit
5d16984e4e
9 files changed
+772
-190
No files matched your search
+112
-79
@@ -3,12 +3,16 @@ import 'package:flutter/material.dart';
|
||||
import '../config/responsive.dart';
|
||||
import '../config/strings.dart';
|
||||
import '../models/session_model.dart';
|
||||
import '../models/session_share.dart';
|
||||
import '../services/file_picker_service.dart';
|
||||
import '../services/qr_service.dart';
|
||||
import '../widgets/app_notice.dart';
|
||||
import 'home_page.dart';
|
||||
import 'settings_page.dart';
|
||||
|
||||
/// 登录页:粘贴 / 识别 QR 令牌,走 Aime 登录后进入主页。
|
||||
///
|
||||
/// 也接受主页导出的「连接信息」(实验性):那是一串 Base64,直接续上原有会话。
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@@ -19,6 +23,7 @@ class LoginPage extends StatefulWidget {
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _qrController = TextEditingController();
|
||||
String _qrCode = '';
|
||||
SessionShare? _share;
|
||||
bool _loading = false;
|
||||
bool _forcePreviewApi = false;
|
||||
|
||||
@@ -29,8 +34,10 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
|
||||
void _onQRContentChanged(String value) {
|
||||
final text = value.trim();
|
||||
setState(() {
|
||||
_qrCode = SessionModel.extractQRCode(value.trim());
|
||||
_qrCode = SessionModel.extractQRCode(text);
|
||||
_share = SessionShare.tryDecode(text);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,6 +52,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
_qrController.text = result;
|
||||
setState(() {
|
||||
_qrCode = SessionModel.extractQRCode(result);
|
||||
_share = SessionShare.tryDecode(result);
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -70,44 +78,85 @@ class _LoginPageState extends State<LoginPage> {
|
||||
Future<void> _onLogin() async {
|
||||
if (_qrCode.isEmpty) return;
|
||||
|
||||
setState(() => _loading = true);
|
||||
if (_qrCode.startsWith("SGWCMAID")) {
|
||||
setState(() => _loading = true);
|
||||
|
||||
try {
|
||||
final result = await SessionModel.instance.loginWithQr(_qrCode);
|
||||
try {
|
||||
final result = await SessionModel.instance.loginWithQr(_qrCode);
|
||||
|
||||
if (!mounted) return;
|
||||
if (!mounted) return;
|
||||
|
||||
if (result.success) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HomePage(
|
||||
userId: result.userId,
|
||||
token: result.token,
|
||||
forcePreviewApi: _forcePreviewApi,
|
||||
if (result.success) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HomePage(
|
||||
userId: result.userId,
|
||||
token: result.token,
|
||||
forcePreviewApi: _forcePreviewApi,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${AppStrings.loginFailed} (errorID: ${result.errorId})')),
|
||||
);
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${AppStrings.loginFailed} (errorID: ${result.errorId})',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${AppStrings.requestFailed}: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 实验性:用主页导出的连接信息恢复会话。带可用 JSESSIONID 就直接续上那次登录,
|
||||
/// 否则退回用令牌重新登录。
|
||||
Future<void> _onRestoreSession() async {
|
||||
final share = _share;
|
||||
if (share == null) return;
|
||||
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final result = await SessionModel.instance.restoreFromShare(share);
|
||||
if (!mounted) return;
|
||||
context.showSnack(
|
||||
result.reusedCookie
|
||||
? AppStrings.sessionRestoreCookie
|
||||
: AppStrings.sessionRestoreToken,
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HomePage(
|
||||
userId: share.userId,
|
||||
token: share.token,
|
||||
sessionRestored: true,
|
||||
initialUserData: result.userData,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${AppStrings.requestFailed}: $e')),
|
||||
);
|
||||
context.showSnack('${AppStrings.sessionRestoreFailed}: $e');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isRestore = _share != null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -117,9 +166,9 @@ class _LoginPageState extends State<LoginPage> {
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: AppStrings.settingsTooltip,
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SettingsPage()),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => const SettingsPage()));
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -145,14 +194,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
AppStrings.loginSubtitle,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
Card(
|
||||
elevation: 0,
|
||||
@@ -194,6 +236,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: AppStrings.qrHint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
@@ -205,40 +248,14 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
),
|
||||
|
||||
if (_qrCode.isNotEmpty) ...[
|
||||
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(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppStrings.qrParsedResult,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
_qrCode,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
if (isRestore)
|
||||
const AppNotice(
|
||||
AppStrings.sessionShareDetected,
|
||||
icon: Icons.science_outlined,
|
||||
),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -257,8 +274,11 @@ class _LoginPageState extends State<LoginPage> {
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed:
|
||||
(_qrCode.isNotEmpty && !_loading) ? _onLogin : null,
|
||||
onPressed: isRestore
|
||||
? (_loading ? null : _onRestoreSession)
|
||||
: (_qrCode.isNotEmpty && !_loading)
|
||||
? _onLogin
|
||||
: null,
|
||||
icon: _loading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
@@ -268,8 +288,19 @@ class _LoginPageState extends State<LoginPage> {
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.arrow_forward, size: 20),
|
||||
label: Text(_loading ? AppStrings.loggingIn : AppStrings.login),
|
||||
: Icon(
|
||||
isRestore ? Icons.link : Icons.arrow_forward,
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
isRestore
|
||||
? (_loading
|
||||
? AppStrings.restoringSession
|
||||
: AppStrings.restoreSession)
|
||||
: (_loading
|
||||
? AppStrings.loggingIn
|
||||
: AppStrings.login),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -281,14 +312,16 @@ class _LoginPageState extends State<LoginPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CheckboxListTile(
|
||||
value: _forcePreviewApi,
|
||||
onChanged: (v) => setState(() => _forcePreviewApi = v ?? false),
|
||||
title: const Text(AppStrings.forcePreviewApi),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
),
|
||||
if (!isRestore)
|
||||
CheckboxListTile(
|
||||
value: _forcePreviewApi,
|
||||
onChanged: (v) =>
|
||||
setState(() => _forcePreviewApi = v ?? false),
|
||||
title: const Text(AppStrings.forcePreviewApi),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user