游戏1.0提交
This commit is contained in:
parent
73f1cf1199
commit
80e9f3a7d0
@ -1,33 +1,55 @@
|
||||
plugins {
|
||||
id("com.google.gms.google-services") version "4.4.3" apply false
|
||||
}
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
// 使用阿里云镜像加速
|
||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
// 使用阿里云镜像加速
|
||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
|
||||
// 备用官方仓库
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
// 以下是你的现有配置
|
||||
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
plugins {
|
||||
id("com.google.gms.google-services") version "4.4.3" apply false
|
||||
}
|
||||
|
||||
val mirrorRepos = listOf(
|
||||
"https://maven.aliyun.com/repository/google",
|
||||
"https://maven.aliyun.com/repository/public",
|
||||
"https://maven.aliyun.com/repository/gradle-plugin",
|
||||
)
|
||||
|
||||
fun org.gradle.api.artifacts.dsl.RepositoryHandler.addMirrorRepos() {
|
||||
mirrorRepos.forEach { url ->
|
||||
maven(url = uri(url))
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
maven(url = uri("https://maven.aliyun.com/repository/google"))
|
||||
maven(url = uri("https://maven.aliyun.com/repository/public"))
|
||||
maven(url = uri("https://maven.aliyun.com/repository/gradle-plugin"))
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
addMirrorRepos()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
gradle.beforeProject {
|
||||
buildscript.repositories.apply {
|
||||
addMirrorRepos()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
repositories.apply {
|
||||
addMirrorRepos()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
// 以下是你的现有配置
|
||||
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
@ -40,4 +62,4 @@ subprojects {
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dhttps.protocols=TLSv1.2 -Djdk.tls.client.protocols=TLSv1.2 -Djava.net.preferIPv4Stack=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
systemProp.https.protocols=TLSv1.2
|
||||
systemProp.jdk.tls.client.protocols=TLSv1.2
|
||||
systemProp.java.net.preferIPv4Stack=true
|
||||
|
||||
131
assets/debug/baishun_mock/index.html
Normal file
131
assets/debug/baishun_mock/index.html
Normal file
@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||
/>
|
||||
<title>BAISHUN Mock</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: linear-gradient(180deg, #07110f 0%, #11342d 100%);
|
||||
color: #f4fffb;
|
||||
}
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 18px;
|
||||
line-height: 1.5;
|
||||
color: rgba(244, 255, 251, 0.8);
|
||||
}
|
||||
.panel {
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: #2ae0a1;
|
||||
color: #03231c;
|
||||
padding: 10px 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
pre {
|
||||
margin-top: 16px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
min-height: 160px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>BAISHUN Mock H5</h1>
|
||||
<p>
|
||||
This page is only for Flutter bridge bring-up. It exercises
|
||||
getConfig, gameLoaded, gameRecharge, destroy and walletUpdate.
|
||||
</p>
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<button onclick="requestConfig()">getConfig</button>
|
||||
<button onclick="notifyLoaded()">gameLoaded</button>
|
||||
<button onclick="requestRecharge()">gameRecharge</button>
|
||||
<button onclick="requestDestroy()">destroy</button>
|
||||
</div>
|
||||
<pre id="log"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const logEl = document.getElementById("log");
|
||||
|
||||
function writeLog(title, payload) {
|
||||
const line = `[${new Date().toLocaleTimeString()}] ${title}\n${JSON.stringify(
|
||||
payload,
|
||||
null,
|
||||
2
|
||||
)}\n\n`;
|
||||
logEl.textContent = line + logEl.textContent;
|
||||
}
|
||||
|
||||
function requestConfig() {
|
||||
if (!window.NativeBridge || typeof window.NativeBridge.getConfig !== "function") {
|
||||
writeLog("getConfig", { error: "NativeBridge not ready" });
|
||||
return;
|
||||
}
|
||||
window.NativeBridge.getConfig(function (config) {
|
||||
writeLog("getConfig callback", config);
|
||||
});
|
||||
}
|
||||
|
||||
function notifyLoaded() {
|
||||
window.NativeBridge &&
|
||||
window.NativeBridge.gameLoaded &&
|
||||
window.NativeBridge.gameLoaded({ status: "loaded" });
|
||||
writeLog("gameLoaded", { status: "sent" });
|
||||
}
|
||||
|
||||
function requestRecharge() {
|
||||
window.NativeBridge &&
|
||||
window.NativeBridge.gameRecharge &&
|
||||
window.NativeBridge.gameRecharge({ source: "mock-h5" });
|
||||
writeLog("gameRecharge", { source: "mock-h5" });
|
||||
}
|
||||
|
||||
function requestDestroy() {
|
||||
window.NativeBridge &&
|
||||
window.NativeBridge.destroy &&
|
||||
window.NativeBridge.destroy({ source: "mock-h5" });
|
||||
writeLog("destroy", { source: "mock-h5" });
|
||||
}
|
||||
|
||||
window.walletUpdate = function (payload) {
|
||||
writeLog("walletUpdate", payload || {});
|
||||
};
|
||||
|
||||
window.addEventListener("walletUpdate", function (event) {
|
||||
writeLog("walletUpdate event", event.detail || {});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
design_assets/logo/yumi_chat_party_original_logo_1024.png
Normal file
BIN
design_assets/logo/yumi_chat_party_original_logo_1024.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
design_assets/logo/yumi_chat_party_original_logo_512.png
Normal file
BIN
design_assets/logo/yumi_chat_party_original_logo_512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 575 KiB |
116
lib/modules/room_game/bridge/baishun_js_bridge.dart
Normal file
116
lib/modules/room_game/bridge/baishun_js_bridge.dart
Normal file
@ -0,0 +1,116 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
|
||||
class BaishunBridgeActions {
|
||||
static const String getConfig = 'getConfig';
|
||||
static const String gameLoaded = 'gameLoaded';
|
||||
static const String gameRecharge = 'gameRecharge';
|
||||
static const String destroy = 'destroy';
|
||||
}
|
||||
|
||||
class BaishunBridgeMessage {
|
||||
const BaishunBridgeMessage({
|
||||
required this.action,
|
||||
this.payload = const <String, dynamic>{},
|
||||
});
|
||||
|
||||
final String action;
|
||||
final Map<String, dynamic> payload;
|
||||
|
||||
static BaishunBridgeMessage parse(String rawMessage) {
|
||||
try {
|
||||
final dynamic decoded = jsonDecode(rawMessage);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final rawPayload = decoded['payload'];
|
||||
return BaishunBridgeMessage(
|
||||
action: (decoded['action'] ?? '').toString(),
|
||||
payload:
|
||||
rawPayload is Map<String, dynamic>
|
||||
? rawPayload
|
||||
: const <String, dynamic>{},
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
return BaishunBridgeMessage(action: rawMessage.trim());
|
||||
}
|
||||
}
|
||||
|
||||
class BaishunJsBridge {
|
||||
static const String channelName = 'BaishunBridgeChannel';
|
||||
|
||||
static String bootstrapScript() {
|
||||
return '''
|
||||
(function() {
|
||||
if (window.__baishunBridgeReady) {
|
||||
return;
|
||||
}
|
||||
window.__baishunBridgeReady = true;
|
||||
window.NativeBridge = window.NativeBridge || {};
|
||||
window.NativeBridge.getConfig = function(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
window.__baishunGetConfigCallback = callback;
|
||||
}
|
||||
$channelName.postMessage(JSON.stringify({ action: '${BaishunBridgeActions.getConfig}' }));
|
||||
return window.__baishunLastConfig || null;
|
||||
};
|
||||
window.NativeBridge.destroy = function(payload) {
|
||||
$channelName.postMessage(JSON.stringify({ action: '${BaishunBridgeActions.destroy}', payload: payload || {} }));
|
||||
};
|
||||
window.NativeBridge.gameRecharge = function(payload) {
|
||||
$channelName.postMessage(JSON.stringify({ action: '${BaishunBridgeActions.gameRecharge}', payload: payload || {} }));
|
||||
};
|
||||
window.NativeBridge.gameLoaded = function(payload) {
|
||||
$channelName.postMessage(JSON.stringify({ action: '${BaishunBridgeActions.gameLoaded}', payload: payload || {} }));
|
||||
};
|
||||
window.baishunChannel = window.baishunChannel || {};
|
||||
window.baishunChannel.walletUpdate = function(payload) {
|
||||
window.__baishunLastWalletPayload = payload || {};
|
||||
if (typeof window.walletUpdate === 'function') {
|
||||
window.walletUpdate(payload || {});
|
||||
}
|
||||
if (typeof window.onWalletUpdate === 'function') {
|
||||
window.onWalletUpdate(payload || {});
|
||||
}
|
||||
if (typeof window.dispatchEvent === 'function') {
|
||||
window.dispatchEvent(new CustomEvent('walletUpdate', { detail: payload || {} }));
|
||||
}
|
||||
};
|
||||
})();
|
||||
''';
|
||||
}
|
||||
|
||||
static String buildConfigScript(BaishunBridgeConfigModel config) {
|
||||
final payload = jsonEncode(config.toJson());
|
||||
return '''
|
||||
(function() {
|
||||
const config = $payload;
|
||||
window.__baishunLastConfig = config;
|
||||
if (typeof window.__baishunGetConfigCallback === 'function') {
|
||||
window.__baishunGetConfigCallback(config);
|
||||
return;
|
||||
}
|
||||
if (typeof window.onBaishunConfig === 'function') {
|
||||
window.onBaishunConfig(config);
|
||||
}
|
||||
})();
|
||||
''';
|
||||
}
|
||||
|
||||
static String buildWalletUpdateScript({Map<String, dynamic>? payload}) {
|
||||
final safePayload = jsonEncode(
|
||||
payload ??
|
||||
<String, dynamic>{
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
},
|
||||
);
|
||||
return '''
|
||||
(function() {
|
||||
const payload = $safePayload;
|
||||
if (window.baishunChannel && typeof window.baishunChannel.walletUpdate === 'function') {
|
||||
window.baishunChannel.walletUpdate(payload);
|
||||
}
|
||||
})();
|
||||
''';
|
||||
}
|
||||
}
|
||||
341
lib/modules/room_game/data/models/room_game_models.dart
Normal file
341
lib/modules/room_game/data/models/room_game_models.dart
Normal file
@ -0,0 +1,341 @@
|
||||
class RoomGameShortcutModel {
|
||||
const RoomGameShortcutModel({
|
||||
required this.gameId,
|
||||
required this.vendorType,
|
||||
required this.vendorGameId,
|
||||
required this.name,
|
||||
required this.cover,
|
||||
required this.launchMode,
|
||||
required this.gameMode,
|
||||
});
|
||||
|
||||
final String gameId;
|
||||
final String vendorType;
|
||||
final int vendorGameId;
|
||||
final String name;
|
||||
final String cover;
|
||||
final String launchMode;
|
||||
final int gameMode;
|
||||
|
||||
factory RoomGameShortcutModel.fromJson(Map<String, dynamic> json) {
|
||||
return RoomGameShortcutModel(
|
||||
gameId: _asString(json['gameId']),
|
||||
vendorType: _asString(json['vendorType']),
|
||||
vendorGameId: _asInt(json['vendorGameId']),
|
||||
name: _asString(json['name']),
|
||||
cover: _asString(json['cover']),
|
||||
launchMode: _asString(json['launchMode']),
|
||||
gameMode: _asInt(json['gameMode']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomGameListItemModel {
|
||||
const RoomGameListItemModel({
|
||||
required this.gameId,
|
||||
required this.vendorType,
|
||||
required this.vendorGameId,
|
||||
required this.name,
|
||||
required this.cover,
|
||||
required this.category,
|
||||
required this.sort,
|
||||
required this.launchMode,
|
||||
required this.fullScreen,
|
||||
required this.gameMode,
|
||||
required this.safeHeight,
|
||||
required this.orientation,
|
||||
required this.packageVersion,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final String gameId;
|
||||
final String vendorType;
|
||||
final int vendorGameId;
|
||||
final String name;
|
||||
final String cover;
|
||||
final String category;
|
||||
final int sort;
|
||||
final String launchMode;
|
||||
final bool fullScreen;
|
||||
final int gameMode;
|
||||
final int safeHeight;
|
||||
final int orientation;
|
||||
final String packageVersion;
|
||||
final String status;
|
||||
|
||||
bool get isBaishun => vendorType.toUpperCase() == 'BAISHUN';
|
||||
|
||||
factory RoomGameListItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return RoomGameListItemModel(
|
||||
gameId: _asString(json['gameId']),
|
||||
vendorType: _asString(json['vendorType']),
|
||||
vendorGameId: _asInt(json['vendorGameId']),
|
||||
name: _asString(json['name']),
|
||||
cover: _asString(json['cover']),
|
||||
category: _asString(json['category']),
|
||||
sort: _asInt(json['sort']),
|
||||
launchMode: _asString(json['launchMode']),
|
||||
fullScreen: _asBool(json['fullScreen']),
|
||||
gameMode: _asInt(json['gameMode']),
|
||||
safeHeight: _asInt(json['safeHeight']),
|
||||
orientation: _asInt(json['orientation']),
|
||||
packageVersion: _asString(json['packageVersion']),
|
||||
status: _asString(json['status']),
|
||||
);
|
||||
}
|
||||
|
||||
factory RoomGameListItemModel.debugMock() {
|
||||
return const RoomGameListItemModel(
|
||||
gameId: 'bs_mock',
|
||||
vendorType: 'BAISHUN',
|
||||
vendorGameId: 999001,
|
||||
name: 'BAISHUN Mock',
|
||||
cover: '',
|
||||
category: 'CHAT_ROOM',
|
||||
sort: 999999,
|
||||
launchMode: 'H5_REMOTE',
|
||||
fullScreen: true,
|
||||
gameMode: 3,
|
||||
safeHeight: 800,
|
||||
orientation: 1,
|
||||
packageVersion: 'debug',
|
||||
status: 'DEBUG',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomGameListResponseModel {
|
||||
const RoomGameListResponseModel({required this.items});
|
||||
|
||||
final List<RoomGameListItemModel> items;
|
||||
|
||||
factory RoomGameListResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
final list = json['items'] as List<dynamic>? ?? const <dynamic>[];
|
||||
return RoomGameListResponseModel(
|
||||
items:
|
||||
list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(RoomGameListItemModel.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BaishunGameConfigModel {
|
||||
const BaishunGameConfigModel({
|
||||
required this.sceneMode,
|
||||
required this.currencyIcon,
|
||||
});
|
||||
|
||||
final int sceneMode;
|
||||
final String currencyIcon;
|
||||
|
||||
factory BaishunGameConfigModel.fromJson(Map<String, dynamic> json) {
|
||||
return BaishunGameConfigModel(
|
||||
sceneMode: _asInt(json['sceneMode']),
|
||||
currencyIcon: _asString(json['currencyIcon']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'sceneMode': sceneMode, 'currencyIcon': currencyIcon};
|
||||
}
|
||||
}
|
||||
|
||||
class BaishunBridgeConfigModel {
|
||||
const BaishunBridgeConfigModel({
|
||||
required this.appName,
|
||||
required this.appChannel,
|
||||
required this.appId,
|
||||
required this.userId,
|
||||
required this.code,
|
||||
required this.roomId,
|
||||
required this.gameMode,
|
||||
required this.language,
|
||||
required this.gsp,
|
||||
required this.gameConfig,
|
||||
});
|
||||
|
||||
final String appName;
|
||||
final String appChannel;
|
||||
final int appId;
|
||||
final String userId;
|
||||
final String code;
|
||||
final String roomId;
|
||||
final String gameMode;
|
||||
final String language;
|
||||
final int gsp;
|
||||
final BaishunGameConfigModel gameConfig;
|
||||
|
||||
factory BaishunBridgeConfigModel.fromJson(Map<String, dynamic> json) {
|
||||
return BaishunBridgeConfigModel(
|
||||
appName: _asString(json['appName']),
|
||||
appChannel: _asString(json['appChannel']),
|
||||
appId: _asInt(json['appId']),
|
||||
userId: _asString(json['userId']),
|
||||
code: _asString(json['code']),
|
||||
roomId: _asString(json['roomId']),
|
||||
gameMode: _asString(json['gameMode']),
|
||||
language: _asString(json['language']),
|
||||
gsp: _asInt(json['gsp']),
|
||||
gameConfig: BaishunGameConfigModel.fromJson(
|
||||
json['gameConfig'] as Map<String, dynamic>? ??
|
||||
const <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'appName': appName,
|
||||
'appChannel': appChannel,
|
||||
'appId': appId,
|
||||
'userId': userId,
|
||||
'code': code,
|
||||
'roomId': roomId,
|
||||
'gameMode': gameMode,
|
||||
'language': language,
|
||||
'gsp': gsp,
|
||||
'gameConfig': gameConfig.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BaishunLaunchEntryModel {
|
||||
const BaishunLaunchEntryModel({
|
||||
required this.launchMode,
|
||||
required this.entryUrl,
|
||||
required this.previewUrl,
|
||||
required this.downloadUrl,
|
||||
required this.packageVersion,
|
||||
required this.orientation,
|
||||
required this.safeHeight,
|
||||
});
|
||||
|
||||
final String launchMode;
|
||||
final String entryUrl;
|
||||
final String previewUrl;
|
||||
final String downloadUrl;
|
||||
final String packageVersion;
|
||||
final int orientation;
|
||||
final int safeHeight;
|
||||
|
||||
factory BaishunLaunchEntryModel.fromJson(Map<String, dynamic> json) {
|
||||
return BaishunLaunchEntryModel(
|
||||
launchMode: _asString(json['launchMode']),
|
||||
entryUrl: _asString(json['entryUrl']),
|
||||
previewUrl: _asString(json['previewUrl']),
|
||||
downloadUrl: _asString(json['downloadUrl']),
|
||||
packageVersion: _asString(json['packageVersion']),
|
||||
orientation: _asInt(json['orientation']),
|
||||
safeHeight: _asInt(json['safeHeight']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BaishunRoomStateModel {
|
||||
const BaishunRoomStateModel({
|
||||
required this.roomId,
|
||||
required this.state,
|
||||
required this.gameSessionId,
|
||||
required this.currentGameId,
|
||||
required this.currentVendorGameId,
|
||||
required this.currentGameName,
|
||||
required this.currentGameCover,
|
||||
required this.hostUserId,
|
||||
});
|
||||
|
||||
final String roomId;
|
||||
final String state;
|
||||
final String gameSessionId;
|
||||
final String currentGameId;
|
||||
final int currentVendorGameId;
|
||||
final String currentGameName;
|
||||
final String currentGameCover;
|
||||
final int hostUserId;
|
||||
|
||||
factory BaishunRoomStateModel.fromJson(Map<String, dynamic> json) {
|
||||
return BaishunRoomStateModel(
|
||||
roomId: _asString(json['roomId']),
|
||||
state: _asString(json['state']),
|
||||
gameSessionId: _asString(json['gameSessionId']),
|
||||
currentGameId: _asString(json['currentGameId']),
|
||||
currentVendorGameId: _asInt(json['currentVendorGameId']),
|
||||
currentGameName: _asString(json['currentGameName']),
|
||||
currentGameCover: _asString(json['currentGameCover']),
|
||||
hostUserId: _asInt(json['hostUserId']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BaishunLaunchModel {
|
||||
const BaishunLaunchModel({
|
||||
required this.gameSessionId,
|
||||
required this.vendorType,
|
||||
required this.gameId,
|
||||
required this.vendorGameId,
|
||||
required this.entry,
|
||||
required this.bridgeConfig,
|
||||
required this.roomState,
|
||||
});
|
||||
|
||||
final String gameSessionId;
|
||||
final String vendorType;
|
||||
final String gameId;
|
||||
final int vendorGameId;
|
||||
final BaishunLaunchEntryModel entry;
|
||||
final BaishunBridgeConfigModel bridgeConfig;
|
||||
final BaishunRoomStateModel roomState;
|
||||
|
||||
factory BaishunLaunchModel.fromJson(Map<String, dynamic> json) {
|
||||
return BaishunLaunchModel(
|
||||
gameSessionId: _asString(json['gameSessionId']),
|
||||
vendorType: _asString(json['vendorType']),
|
||||
gameId: _asString(json['gameId']),
|
||||
vendorGameId: _asInt(json['vendorGameId']),
|
||||
entry: BaishunLaunchEntryModel.fromJson(
|
||||
json['entry'] as Map<String, dynamic>? ?? const <String, dynamic>{},
|
||||
),
|
||||
bridgeConfig: BaishunBridgeConfigModel.fromJson(
|
||||
json['bridgeConfig'] as Map<String, dynamic>? ??
|
||||
const <String, dynamic>{},
|
||||
),
|
||||
roomState: BaishunRoomStateModel.fromJson(
|
||||
json['roomState'] as Map<String, dynamic>? ?? const <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _asString(dynamic value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
int _asInt(dynamic value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(value.toString()) ?? 0;
|
||||
}
|
||||
|
||||
bool _asBool(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
return value.toLowerCase() == 'true' || value == '1';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
115
lib/modules/room_game/data/room_game_api.dart
Normal file
115
lib/modules/room_game/data/room_game_api.dart
Normal file
@ -0,0 +1,115 @@
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||
|
||||
const String _gameApiHost = String.fromEnvironment(
|
||||
'GAME_API_HOST',
|
||||
defaultValue: '',
|
||||
);
|
||||
|
||||
class RoomGameApi {
|
||||
RoomGameApi({NetworkClient? client}) : _client = client ?? http;
|
||||
|
||||
final NetworkClient _client;
|
||||
|
||||
Map<String, dynamic>? _buildExtra([Map<String, dynamic>? extra]) {
|
||||
final merged = <String, dynamic>{...?extra};
|
||||
final trimmedGameApiHost = _gameApiHost.trim();
|
||||
if (trimmedGameApiHost.isNotEmpty) {
|
||||
merged[BaseNetworkClient.baseUrlOverrideKey] = trimmedGameApiHost;
|
||||
}
|
||||
return merged.isEmpty ? null : merged;
|
||||
}
|
||||
|
||||
Future<List<RoomGameShortcutModel>> fetchShortcutGames({
|
||||
required String roomId,
|
||||
}) {
|
||||
return _client.get<List<RoomGameShortcutModel>>(
|
||||
'/app/game/room/shortcut',
|
||||
queryParams: {'roomId': roomId},
|
||||
extra: _buildExtra(),
|
||||
fromJson: (dynamic json) {
|
||||
final list = json as List<dynamic>? ?? const <dynamic>[];
|
||||
return list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(RoomGameShortcutModel.fromJson)
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<RoomGameListResponseModel> fetchRoomGames({
|
||||
required String roomId,
|
||||
String category = '',
|
||||
}) {
|
||||
final query = <String, dynamic>{'roomId': roomId};
|
||||
if (category.isNotEmpty) {
|
||||
query['category'] = category;
|
||||
}
|
||||
return _client.get<RoomGameListResponseModel>(
|
||||
'/app/game/room/list',
|
||||
queryParams: query,
|
||||
extra: _buildExtra(const <String, dynamic>{
|
||||
BaseNetworkClient.silentErrorToastKey: true,
|
||||
}),
|
||||
fromJson:
|
||||
(dynamic json) => RoomGameListResponseModel.fromJson(
|
||||
json as Map<String, dynamic>? ?? const <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<BaishunRoomStateModel> fetchRoomState({required String roomId}) {
|
||||
return _client.get<BaishunRoomStateModel>(
|
||||
'/app/game/baishun/state',
|
||||
queryParams: {'roomId': roomId},
|
||||
extra: _buildExtra(),
|
||||
fromJson:
|
||||
(dynamic json) => BaishunRoomStateModel.fromJson(
|
||||
json as Map<String, dynamic>? ?? const <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<BaishunLaunchModel> launchBaishunGame({
|
||||
required String roomId,
|
||||
required String gameId,
|
||||
required int sceneMode,
|
||||
required String clientOrigin,
|
||||
}) {
|
||||
return _client.post<BaishunLaunchModel>(
|
||||
'/app/game/baishun/launch',
|
||||
data: {
|
||||
'roomId': roomId,
|
||||
'gameId': gameId,
|
||||
'sceneMode': sceneMode,
|
||||
'clientOrigin': clientOrigin,
|
||||
},
|
||||
extra: _buildExtra(),
|
||||
fromJson:
|
||||
(dynamic json) => BaishunLaunchModel.fromJson(
|
||||
json as Map<String, dynamic>? ?? const <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<BaishunRoomStateModel> closeBaishunGame({
|
||||
required String roomId,
|
||||
required String gameSessionId,
|
||||
String reason = 'user_exit',
|
||||
}) {
|
||||
return _client.post<BaishunRoomStateModel>(
|
||||
'/app/game/baishun/close',
|
||||
data: {
|
||||
'roomId': roomId,
|
||||
'gameSessionId': gameSessionId,
|
||||
'reason': reason,
|
||||
},
|
||||
extra: _buildExtra(),
|
||||
fromJson:
|
||||
(dynamic json) => BaishunRoomStateModel.fromJson(
|
||||
json as Map<String, dynamic>? ?? const <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
lib/modules/room_game/data/room_game_repository.dart
Normal file
52
lib/modules/room_game/data/room_game_repository.dart
Normal file
@ -0,0 +1,52 @@
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
import 'package:yumi/modules/room_game/data/room_game_api.dart';
|
||||
|
||||
class RoomGameRepository {
|
||||
RoomGameRepository({RoomGameApi? api}) : _api = api ?? RoomGameApi();
|
||||
|
||||
final RoomGameApi _api;
|
||||
|
||||
Future<List<RoomGameShortcutModel>> fetchShortcutGames({
|
||||
required String roomId,
|
||||
}) {
|
||||
return _api.fetchShortcutGames(roomId: roomId);
|
||||
}
|
||||
|
||||
Future<List<RoomGameListItemModel>> fetchRoomGames({
|
||||
required String roomId,
|
||||
String category = '',
|
||||
}) async {
|
||||
final response = await _api.fetchRoomGames(roomId: roomId, category: category);
|
||||
return response.items;
|
||||
}
|
||||
|
||||
Future<BaishunRoomStateModel> fetchRoomState({required String roomId}) {
|
||||
return _api.fetchRoomState(roomId: roomId);
|
||||
}
|
||||
|
||||
Future<BaishunLaunchModel> launchBaishunGame({
|
||||
required String roomId,
|
||||
required String gameId,
|
||||
int sceneMode = 0,
|
||||
required String clientOrigin,
|
||||
}) {
|
||||
return _api.launchBaishunGame(
|
||||
roomId: roomId,
|
||||
gameId: gameId,
|
||||
sceneMode: sceneMode,
|
||||
clientOrigin: clientOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
Future<BaishunRoomStateModel> closeBaishunGame({
|
||||
required String roomId,
|
||||
required String gameSessionId,
|
||||
String reason = 'user_exit',
|
||||
}) {
|
||||
return _api.closeBaishunGame(
|
||||
roomId: roomId,
|
||||
gameSessionId: gameSessionId,
|
||||
reason: reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
296
lib/modules/room_game/views/baishun_game_page.dart
Normal file
296
lib/modules/room_game/views/baishun_game_page.dart
Normal file
@ -0,0 +1,296 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/modules/room_game/bridge/baishun_js_bridge.dart';
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
import 'package:yumi/modules/room_game/data/room_game_repository.dart';
|
||||
import 'package:yumi/modules/room_game/views/baishun_loading_view.dart';
|
||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
|
||||
class BaishunGamePage extends StatefulWidget {
|
||||
const BaishunGamePage({
|
||||
super.key,
|
||||
required this.roomId,
|
||||
required this.game,
|
||||
required this.launchModel,
|
||||
});
|
||||
|
||||
final String roomId;
|
||||
final RoomGameListItemModel game;
|
||||
final BaishunLaunchModel launchModel;
|
||||
|
||||
@override
|
||||
State<BaishunGamePage> createState() => _BaishunGamePageState();
|
||||
}
|
||||
|
||||
class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
final RoomGameRepository _repository = RoomGameRepository();
|
||||
late final WebViewController _controller;
|
||||
|
||||
bool _isLoading = true;
|
||||
bool _isClosing = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(Colors.black)
|
||||
..addJavaScriptChannel(
|
||||
BaishunJsBridge.channelName,
|
||||
onMessageReceived: _handleBridgeMessage,
|
||||
)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageFinished: (String _) async {
|
||||
await _injectBridge();
|
||||
},
|
||||
onWebResourceError: (WebResourceError error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_errorMessage = error.description;
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
unawaited(_loadGameEntry());
|
||||
}
|
||||
|
||||
Future<void> _loadGameEntry() async {
|
||||
try {
|
||||
final entryUrl = widget.launchModel.entry.entryUrl.trim();
|
||||
if (entryUrl.isEmpty || entryUrl.startsWith('mock://')) {
|
||||
final html = await rootBundle.loadString(
|
||||
'assets/debug/baishun_mock/index.html',
|
||||
);
|
||||
await _controller.loadHtmlString(
|
||||
html,
|
||||
baseUrl: 'https://baishun.mock.local/',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.tryParse(entryUrl);
|
||||
if (uri == null) {
|
||||
throw Exception('Invalid game entry url: $entryUrl');
|
||||
}
|
||||
await _controller.loadRequest(uri);
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_errorMessage = error.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _injectBridge() async {
|
||||
try {
|
||||
await _controller.runJavaScript(BaishunJsBridge.bootstrapScript());
|
||||
await _controller.runJavaScript(
|
||||
BaishunJsBridge.buildConfigScript(widget.launchModel.bridgeConfig),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _handleBridgeMessage(JavaScriptMessage message) async {
|
||||
final bridgeMessage = BaishunBridgeMessage.parse(message.message);
|
||||
switch (bridgeMessage.action) {
|
||||
case BaishunBridgeActions.getConfig:
|
||||
await _controller.runJavaScript(
|
||||
BaishunJsBridge.buildConfigScript(widget.launchModel.bridgeConfig),
|
||||
);
|
||||
break;
|
||||
case BaishunBridgeActions.gameLoaded:
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = null;
|
||||
});
|
||||
break;
|
||||
case BaishunBridgeActions.gameRecharge:
|
||||
await SCNavigatorUtils.push(context, WalletRoute.recharge);
|
||||
await _notifyWalletUpdate();
|
||||
break;
|
||||
case BaishunBridgeActions.destroy:
|
||||
await _closeAndExit(reason: 'h5_destroy');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _notifyWalletUpdate() async {
|
||||
try {
|
||||
await _controller.runJavaScript(BaishunJsBridge.buildWalletUpdateScript());
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_isLoading = true;
|
||||
});
|
||||
await _loadGameEntry();
|
||||
}
|
||||
|
||||
Future<void> _closeAndExit({String reason = 'page_back'}) async {
|
||||
if (_isClosing) {
|
||||
return;
|
||||
}
|
||||
_isClosing = true;
|
||||
try {
|
||||
if (!widget.launchModel.gameSessionId.startsWith('bs_mock_')) {
|
||||
await _repository.closeBaishunGame(
|
||||
roomId: widget.roomId,
|
||||
gameSessionId: widget.launchModel.gameSessionId,
|
||||
reason: reason,
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||
if (didPop) {
|
||||
return;
|
||||
}
|
||||
await _closeAndExit();
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(24.w),
|
||||
topRight: Radius.circular(24.w),
|
||||
),
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: ScreenUtil().screenHeight * 0.8,
|
||||
color: const Color(0xFF081915),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 10.w),
|
||||
Container(
|
||||
width: 34.w,
|
||||
height: 4.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.28),
|
||||
borderRadius: BorderRadius.circular(99.w),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _closeAndExit,
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.game.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _closeAndExit,
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: WebViewWidget(controller: _controller)),
|
||||
if (_errorMessage != null) _buildErrorState(),
|
||||
if (_isLoading && _errorMessage == null)
|
||||
const BaishunLoadingView(message: 'Waiting for gameLoaded...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState() {
|
||||
return Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.88),
|
||||
padding: EdgeInsets.symmetric(horizontal: 28.w),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.white70, size: 34.w),
|
||||
SizedBox(height: 12.w),
|
||||
Text(
|
||||
'Game page failed to load',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Text(
|
||||
_errorMessage ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _reload,
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
SCTts.show('Please check game host and package url');
|
||||
},
|
||||
child: const Text('Tips'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
lib/modules/room_game/views/baishun_loading_view.dart
Normal file
47
lib/modules/room_game/views/baishun_loading_view.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class BaishunLoadingView extends StatelessWidget {
|
||||
const BaishunLoadingView({
|
||||
super.key,
|
||||
this.message = 'Loading game...',
|
||||
});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.42),
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 22.w, vertical: 18.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF102D28).withValues(alpha: 0.92),
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CupertinoActivityIndicator(color: Colors.white),
|
||||
SizedBox(height: 10.w),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
338
lib/modules/room_game/views/room_game_list_sheet.dart
Normal file
338
lib/modules/room_game/views/room_game_list_sheet.dart
Normal file
@ -0,0 +1,338 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||
import 'package:yumi/modules/room_game/data/room_game_repository.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/modules/room_game/views/baishun_game_page.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class RoomGameListSheet extends StatefulWidget {
|
||||
const RoomGameListSheet({
|
||||
super.key,
|
||||
required this.roomContext,
|
||||
});
|
||||
|
||||
final BuildContext roomContext;
|
||||
|
||||
@override
|
||||
State<RoomGameListSheet> createState() => _RoomGameListSheetState();
|
||||
}
|
||||
|
||||
class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
static const int _itemsPerRow = 5;
|
||||
|
||||
final RoomGameRepository _repository = RoomGameRepository();
|
||||
late Future<List<RoomGameListItemModel>> _gamesFuture;
|
||||
|
||||
String _roomId = '';
|
||||
String? _launchingGameId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
_roomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '';
|
||||
_gamesFuture = _loadGames();
|
||||
}
|
||||
|
||||
Future<List<RoomGameListItemModel>> _loadGames() async {
|
||||
if (_roomId.isEmpty) {
|
||||
return const <RoomGameListItemModel>[];
|
||||
}
|
||||
|
||||
final items = await _repository.fetchRoomGames(
|
||||
roomId: _roomId,
|
||||
category: 'CHAT_ROOM',
|
||||
);
|
||||
return items;
|
||||
}
|
||||
|
||||
Future<void> _retry() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_gamesFuture = _loadGames();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _openGame(RoomGameListItemModel game) async {
|
||||
if (_roomId.isEmpty) {
|
||||
SCTts.show('roomId is empty');
|
||||
return;
|
||||
}
|
||||
if (!game.isBaishun) {
|
||||
SCTts.show('Only BAISHUN games are wired for now');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_launchingGameId = game.gameId;
|
||||
});
|
||||
|
||||
try {
|
||||
final launchModel = await _repository.launchBaishunGame(
|
||||
roomId: _roomId,
|
||||
gameId: game.gameId,
|
||||
clientOrigin: Platform.isAndroid ? 'ANDROID' : 'IOS',
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 180));
|
||||
if (!widget.roomContext.mounted) {
|
||||
return;
|
||||
}
|
||||
showBottomInBottomDialog(
|
||||
widget.roomContext,
|
||||
BaishunGamePage(
|
||||
roomId: _roomId,
|
||||
game: game,
|
||||
launchModel: launchModel,
|
||||
),
|
||||
barrierColor: Colors.black54,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
} catch (error) {
|
||||
SCTts.show('Launch failed');
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_launchingGameId = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(18.w),
|
||||
topRight: Radius.circular(18.w),
|
||||
),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16),
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: ScreenUtil().screenHeight / 3,
|
||||
color: const Color(0xff09372E).withValues(alpha: 0.88),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 10.w),
|
||||
Container(
|
||||
width: 34.w,
|
||||
height: 4.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.28),
|
||||
borderRadius: BorderRadius.circular(99.w),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 14.w, 16.w, 10.w),
|
||||
child: Row(
|
||||
children: [
|
||||
text(
|
||||
'Games',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
const Spacer(),
|
||||
text(
|
||||
_roomId.isEmpty ? 'Debug room' : 'Room $_roomId',
|
||||
fontSize: 11,
|
||||
textColor: Colors.white70,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<RoomGameListItemModel>>(
|
||||
future: _gamesFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return _buildErrorState();
|
||||
}
|
||||
final items =
|
||||
snapshot.data ?? const <RoomGameListItemModel>[];
|
||||
if (items.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final rowCount = (items.length / _itemsPerRow).ceil();
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.fromLTRB(12.w, 0, 12.w, 20.w),
|
||||
itemCount: rowCount,
|
||||
itemBuilder: (context, rowIndex) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 12.w),
|
||||
child: Row(
|
||||
children: List.generate(_itemsPerRow, (columnIndex) {
|
||||
final itemIndex =
|
||||
rowIndex * _itemsPerRow + columnIndex;
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child:
|
||||
itemIndex < items.length
|
||||
? _RoomGameTile(
|
||||
item: items[itemIndex],
|
||||
loading:
|
||||
_launchingGameId ==
|
||||
items[itemIndex].gameId,
|
||||
onTap: () => _openGame(items[itemIndex]),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text('Game list failed', fontSize: 13, textColor: Colors.white),
|
||||
SizedBox(height: 8.w),
|
||||
TextButton(onPressed: _retry, child: const Text('Retry')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: text(
|
||||
'No game configured',
|
||||
fontSize: 13,
|
||||
textColor: Colors.white70,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomGameTile extends StatelessWidget {
|
||||
const _RoomGameTile({
|
||||
required this.item,
|
||||
required this.onTap,
|
||||
required this.loading,
|
||||
});
|
||||
|
||||
final RoomGameListItemModel item;
|
||||
final VoidCallback onTap;
|
||||
final bool loading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: loading ? null : onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
const Color(0xff18F2B1).withValues(alpha: 0.26),
|
||||
Colors.white.withValues(alpha: 0.08),
|
||||
],
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _buildCover(),
|
||||
),
|
||||
if (loading)
|
||||
Container(
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.black.withValues(alpha: 0.45),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 16.w,
|
||||
height: 16.w,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
text(
|
||||
item.name,
|
||||
fontSize: 11,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCover() {
|
||||
if (item.cover.startsWith('http://') || item.cover.startsWith('https://')) {
|
||||
return CustomCachedImage(
|
||||
imageUrl: item.cover,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
fit: BoxFit.cover,
|
||||
borderRadius: 24.w,
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
Icons.sports_esports_outlined,
|
||||
size: 22.w,
|
||||
color: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
// api.dart
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/modules/auth/login_route.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
@ -20,91 +20,94 @@ export 'package:dio/dio.dart';
|
||||
|
||||
_parseAndDecode(String response) => jsonDecode(response);
|
||||
|
||||
parseJson(String text) => compute(_parseAndDecode, text);
|
||||
|
||||
String _normalizeAuthorizationHeader(Object? authorization) {
|
||||
if (authorization == null) {
|
||||
return "";
|
||||
}
|
||||
if (authorization is Iterable) {
|
||||
return authorization.map((value) => value.toString()).join(",");
|
||||
}
|
||||
return authorization.toString().trim();
|
||||
}
|
||||
|
||||
bool _shouldLogoutForUnauthorized(DioException e) {
|
||||
final currentToken = AccountStorage().getToken();
|
||||
if (currentToken.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final requestAuthorization = _normalizeAuthorizationHeader(
|
||||
e.requestOptions.headers["Authorization"],
|
||||
);
|
||||
if (requestAuthorization.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return requestAuthorization == "Bearer $currentToken";
|
||||
}
|
||||
|
||||
Future<bool> _isCurrentSessionStillValid(DioException e) async {
|
||||
if (!_shouldLogoutForUnauthorized(e)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final verificationHeaders = Map<String, dynamic>.from(e.requestOptions.headers)
|
||||
..remove("Content-Length")
|
||||
..remove("content-length");
|
||||
|
||||
final verificationClient = Dio(
|
||||
BaseOptions(
|
||||
baseUrl:
|
||||
e.requestOptions.baseUrl.isNotEmpty
|
||||
? e.requestOptions.baseUrl
|
||||
: SCGlobalConfig.apiHost,
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 5),
|
||||
sendTimeout: const Duration(seconds: 5),
|
||||
responseType: ResponseType.json,
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final verificationResponse = await verificationClient.get(
|
||||
"/app/h5/identity",
|
||||
options: Options(headers: verificationHeaders),
|
||||
);
|
||||
final responseData = verificationResponse.data;
|
||||
|
||||
if (verificationResponse.statusCode == 401) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
if (responseData["status"] == true) {
|
||||
return true;
|
||||
}
|
||||
if (responseData["errorCode"] == SCErroCode.authUnauthorized.code) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} on DioException catch (verificationError) {
|
||||
final responseData = verificationError.response?.data;
|
||||
if (verificationError.response?.statusCode == 401) {
|
||||
return false;
|
||||
}
|
||||
if (responseData is Map<String, dynamic> &&
|
||||
responseData["errorCode"] == SCErroCode.authUnauthorized.code) {
|
||||
return false;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 校验结果不明确时不要主动登出,避免单条接口抖动把整次登录踢掉。
|
||||
return true;
|
||||
}
|
||||
|
||||
class BaseNetworkClient {
|
||||
parseJson(String text) => compute(_parseAndDecode, text);
|
||||
|
||||
String _normalizeAuthorizationHeader(Object? authorization) {
|
||||
if (authorization == null) {
|
||||
return "";
|
||||
}
|
||||
if (authorization is Iterable) {
|
||||
return authorization.map((value) => value.toString()).join(",");
|
||||
}
|
||||
return authorization.toString().trim();
|
||||
}
|
||||
|
||||
bool _shouldLogoutForUnauthorized(DioException e) {
|
||||
final currentToken = AccountStorage().getToken();
|
||||
if (currentToken.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final requestAuthorization = _normalizeAuthorizationHeader(
|
||||
e.requestOptions.headers["Authorization"],
|
||||
);
|
||||
if (requestAuthorization.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return requestAuthorization == "Bearer $currentToken";
|
||||
}
|
||||
|
||||
Future<bool> _isCurrentSessionStillValid(DioException e) async {
|
||||
if (!_shouldLogoutForUnauthorized(e)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final verificationHeaders =
|
||||
Map<String, dynamic>.from(e.requestOptions.headers)
|
||||
..remove("Content-Length")
|
||||
..remove("content-length");
|
||||
|
||||
final verificationClient = Dio(
|
||||
BaseOptions(
|
||||
baseUrl:
|
||||
e.requestOptions.baseUrl.isNotEmpty
|
||||
? e.requestOptions.baseUrl
|
||||
: SCGlobalConfig.apiHost,
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 5),
|
||||
sendTimeout: const Duration(seconds: 5),
|
||||
responseType: ResponseType.json,
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final verificationResponse = await verificationClient.get(
|
||||
"/app/h5/identity",
|
||||
options: Options(headers: verificationHeaders),
|
||||
);
|
||||
final responseData = verificationResponse.data;
|
||||
|
||||
if (verificationResponse.statusCode == 401) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
if (responseData["status"] == true) {
|
||||
return true;
|
||||
}
|
||||
if (responseData["errorCode"] == SCErroCode.authUnauthorized.code) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} on DioException catch (verificationError) {
|
||||
final responseData = verificationError.response?.data;
|
||||
if (verificationError.response?.statusCode == 401) {
|
||||
return false;
|
||||
}
|
||||
if (responseData is Map<String, dynamic> &&
|
||||
responseData["errorCode"] == SCErroCode.authUnauthorized.code) {
|
||||
return false;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 校验结果不明确时不要主动登出,避免单条接口抖动把整次登录踢掉。
|
||||
return true;
|
||||
}
|
||||
|
||||
class BaseNetworkClient {
|
||||
final Dio dio;
|
||||
static const String silentErrorToastKey = 'silentErrorToast';
|
||||
static const String baseUrlOverrideKey = 'baseUrlOverride';
|
||||
|
||||
BaseNetworkClient() : dio = Dio() {
|
||||
_confD();
|
||||
@ -112,13 +115,13 @@ class BaseNetworkClient {
|
||||
}
|
||||
|
||||
void _confD() {
|
||||
dio.transformer = BackgroundTransformer()..jsonDecodeCallback = parseJson;
|
||||
dio.options = BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 12),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
contentType: 'application/json; charset=UTF-8',
|
||||
);
|
||||
}
|
||||
dio.transformer = BackgroundTransformer()..jsonDecodeCallback = parseJson;
|
||||
dio.options = BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 12),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
contentType: 'application/json; charset=UTF-8',
|
||||
);
|
||||
}
|
||||
|
||||
void init() {}
|
||||
|
||||
@ -126,6 +129,7 @@ class BaseNetworkClient {
|
||||
Future<T> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
required T Function(dynamic) fromJson,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
@ -135,6 +139,7 @@ class BaseNetworkClient {
|
||||
path,
|
||||
method: 'GET',
|
||||
queryParams: queryParams,
|
||||
extra: extra,
|
||||
fromJson: fromJson,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
@ -147,6 +152,7 @@ class BaseNetworkClient {
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
required T Function(dynamic) fromJson,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
@ -157,6 +163,7 @@ class BaseNetworkClient {
|
||||
method: 'PUT',
|
||||
data: data,
|
||||
queryParams: queryParams,
|
||||
extra: extra,
|
||||
fromJson: fromJson,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
@ -169,6 +176,7 @@ class BaseNetworkClient {
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
required T Function(dynamic) fromJson,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
@ -179,6 +187,7 @@ class BaseNetworkClient {
|
||||
method: 'POST',
|
||||
data: data,
|
||||
queryParams: queryParams,
|
||||
extra: extra,
|
||||
fromJson: fromJson,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
@ -191,6 +200,7 @@ class BaseNetworkClient {
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
required T Function(dynamic) fromJson,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
@ -201,6 +211,7 @@ class BaseNetworkClient {
|
||||
method: 'DELETE',
|
||||
data: data,
|
||||
queryParams: queryParams,
|
||||
extra: extra,
|
||||
fromJson: fromJson,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
@ -214,6 +225,7 @@ class BaseNetworkClient {
|
||||
required String method,
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? extra,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
@ -224,7 +236,7 @@ class BaseNetworkClient {
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParams,
|
||||
options: Options(method: method),
|
||||
options: Options(method: method, extra: extra),
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
@ -255,9 +267,9 @@ class BaseNetworkClient {
|
||||
error: '业务错误: ${baseResponse.errorMsg}',
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
// 网络错误处理
|
||||
throw await _hdlErr(e);
|
||||
} on DioException catch (e) {
|
||||
// 网络错误处理
|
||||
throw await _hdlErr(e);
|
||||
} catch (e) {
|
||||
SCLoadingManager.hide();
|
||||
throw Exception('未知错误: $e');
|
||||
@ -265,11 +277,13 @@ class BaseNetworkClient {
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
Future<DioException> _hdlErr(DioException e) async {
|
||||
SCLoadingManager.hide();
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
return DioException(requestOptions: e.requestOptions, error: '连接超时');
|
||||
Future<DioException> _hdlErr(DioException e) async {
|
||||
SCLoadingManager.hide();
|
||||
final bool silentErrorToast =
|
||||
e.requestOptions.extra[BaseNetworkClient.silentErrorToastKey] == true;
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
return DioException(requestOptions: e.requestOptions, error: '连接超时');
|
||||
case DioExceptionType.sendTimeout:
|
||||
return DioException(requestOptions: e.requestOptions, error: '发送超时');
|
||||
case DioExceptionType.receiveTimeout:
|
||||
@ -288,24 +302,26 @@ class BaseNetworkClient {
|
||||
replace: false,
|
||||
);
|
||||
}
|
||||
} else if (errorCode == SCErroCode.authUnauthorized.code) {
|
||||
//token过期
|
||||
final shouldLogout =
|
||||
!SCNavigatorUtils.inLoginPage &&
|
||||
!await _isCurrentSessionStillValid(e);
|
||||
final BuildContext? context = navigatorKey.currentContext;
|
||||
if (context != null && shouldLogout) {
|
||||
AccountStorage().logout(context);
|
||||
SCNavigatorUtils.inLoginPage = true;
|
||||
}
|
||||
} else {
|
||||
} else if (errorCode == SCErroCode.authUnauthorized.code) {
|
||||
//token过期
|
||||
final shouldLogout =
|
||||
!SCNavigatorUtils.inLoginPage &&
|
||||
!await _isCurrentSessionStillValid(e);
|
||||
final BuildContext? context = navigatorKey.currentContext;
|
||||
if (context != null && shouldLogout) {
|
||||
AccountStorage().logout(context);
|
||||
SCNavigatorUtils.inLoginPage = true;
|
||||
}
|
||||
} else {
|
||||
if (errorMsg.toString().endsWith("balance not made")) {
|
||||
BuildContext? context = navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
SCRoomUtils.goRecharge(context);
|
||||
}
|
||||
} else {
|
||||
SCTts.show(errorMsg);
|
||||
if (!silentErrorToast) {
|
||||
SCTts.show(errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return DioException(
|
||||
@ -313,11 +329,11 @@ class BaseNetworkClient {
|
||||
response: e.response,
|
||||
error: '服务器错误: ${e.response?.data["errorCode"]}',
|
||||
);
|
||||
case DioExceptionType.cancel:
|
||||
return DioException(requestOptions: e.requestOptions, error: 'Cancel');
|
||||
default:
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
case DioExceptionType.cancel:
|
||||
return DioException(requestOptions: e.requestOptions, error: 'Cancel');
|
||||
default:
|
||||
return DioException(
|
||||
requestOptions: e.requestOptions,
|
||||
error: 'Net fail',
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import 'package:yumi/shared/tools/sc_deviceId_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/sc_logger.dart';
|
||||
|
||||
NetworkClient get http => _httpInstance;
|
||||
NetworkClient get http => _httpInstance;
|
||||
|
||||
late final NetworkClient _httpInstance = NetworkClient();
|
||||
|
||||
@ -271,11 +271,11 @@ const Map<String, String> _localRouteOverrides = {
|
||||
"/user/vip/ability/update",
|
||||
};
|
||||
|
||||
String _resolveRequestPath(String baseUrl, String path) {
|
||||
// Prefer the known plain-text route when we have one.
|
||||
// This avoids environment-specific AES forwarding mismatches at the gateway.
|
||||
return _localRouteOverrides[path] ?? path;
|
||||
}
|
||||
String _resolveRequestPath(String baseUrl, String path) {
|
||||
// Prefer the known plain-text route when we have one.
|
||||
// This avoids environment-specific AES forwarding mismatches at the gateway.
|
||||
return _localRouteOverrides[path] ?? path;
|
||||
}
|
||||
|
||||
class NetworkClient extends BaseNetworkClient {
|
||||
@override
|
||||
@ -304,7 +304,15 @@ class ApiInterceptor extends Interceptor {
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
// 确保使用最新的 baseUrl
|
||||
options.baseUrl = SCGlobalConfig.apiHost;
|
||||
final dynamic baseUrlOverride =
|
||||
options.extra[BaseNetworkClient.baseUrlOverrideKey];
|
||||
final String resolvedBaseUrl =
|
||||
baseUrlOverride is String &&
|
||||
baseUrlOverride.isNotEmpty &&
|
||||
baseUrlOverride.startsWith('http')
|
||||
? baseUrlOverride
|
||||
: SCGlobalConfig.apiHost;
|
||||
options.baseUrl = resolvedBaseUrl;
|
||||
options.path = _resolveRequestPath(options.baseUrl, options.path);
|
||||
|
||||
String token = AccountStorage().getToken();
|
||||
@ -454,24 +462,24 @@ class ResponseData<T> {
|
||||
}
|
||||
|
||||
/// 超时拦截器
|
||||
class TimeOutInterceptor extends Interceptor {
|
||||
static const int globalTimeoutSeconds = 25;
|
||||
static const String timeoutSecondsKey = "requestTimeoutSeconds";
|
||||
final Map<String, Timer> _timers = {};
|
||||
|
||||
int _resolveTimeoutSeconds(RequestOptions options) {
|
||||
final dynamic customTimeout = options.extra[timeoutSecondsKey];
|
||||
if (customTimeout is int && customTimeout > 0) {
|
||||
return customTimeout;
|
||||
}
|
||||
if (customTimeout is String) {
|
||||
final int? parsed = int.tryParse(customTimeout);
|
||||
if (parsed != null && parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return globalTimeoutSeconds;
|
||||
}
|
||||
class TimeOutInterceptor extends Interceptor {
|
||||
static const int globalTimeoutSeconds = 25;
|
||||
static const String timeoutSecondsKey = "requestTimeoutSeconds";
|
||||
final Map<String, Timer> _timers = {};
|
||||
|
||||
int _resolveTimeoutSeconds(RequestOptions options) {
|
||||
final dynamic customTimeout = options.extra[timeoutSecondsKey];
|
||||
if (customTimeout is int && customTimeout > 0) {
|
||||
return customTimeout;
|
||||
}
|
||||
if (customTimeout is String) {
|
||||
final int? parsed = int.tryParse(customTimeout);
|
||||
if (parsed != null && parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return globalTimeoutSeconds;
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
@ -490,26 +498,23 @@ class TimeOutInterceptor extends Interceptor {
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
// 为每个请求创建超时计时器
|
||||
final cancelToken = options.cancelToken ?? CancelToken();
|
||||
options.cancelToken = cancelToken;
|
||||
final String requestKey = options.uri.toString();
|
||||
final int timeoutSeconds = _resolveTimeoutSeconds(options);
|
||||
|
||||
// 默认 15 秒,上传等特殊请求可通过 extra 覆盖。
|
||||
_timers[requestKey] = Timer(
|
||||
Duration(seconds: timeoutSeconds),
|
||||
() {
|
||||
SCLoadingManager.hide();
|
||||
if (!cancelToken.isCancelled) {
|
||||
cancelToken.cancel('请求超时(${timeoutSeconds}秒)');
|
||||
// 从映射中移除计时器
|
||||
_timers.remove(requestKey);
|
||||
}
|
||||
},
|
||||
);
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
// 为每个请求创建超时计时器
|
||||
final cancelToken = options.cancelToken ?? CancelToken();
|
||||
options.cancelToken = cancelToken;
|
||||
final String requestKey = options.uri.toString();
|
||||
final int timeoutSeconds = _resolveTimeoutSeconds(options);
|
||||
|
||||
// 默认 15 秒,上传等特殊请求可通过 extra 覆盖。
|
||||
_timers[requestKey] = Timer(Duration(seconds: timeoutSeconds), () {
|
||||
SCLoadingManager.hide();
|
||||
if (!cancelToken.isCancelled) {
|
||||
cancelToken.cancel('请求超时(${timeoutSeconds}秒)');
|
||||
// 从映射中移除计时器
|
||||
_timers.remove(requestKey);
|
||||
}
|
||||
});
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/modules/room_game/views/room_game_list_sheet.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class RoomGameEntryButton extends StatelessWidget {
|
||||
const RoomGameEntryButton({super.key});
|
||||
@ -15,7 +13,7 @@ class RoomGameEntryButton extends StatelessWidget {
|
||||
onTap: () {
|
||||
showBottomInBottomDialog(
|
||||
context,
|
||||
const RoomGameBottomSheet(),
|
||||
RoomGameBottomSheet(roomContext: context),
|
||||
barrierColor: Colors.black54,
|
||||
);
|
||||
},
|
||||
@ -39,8 +37,6 @@ class RoomGameEntryButton extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
// 当前先用代码绘制的 `🎮` 作为语言房游戏入口占位,
|
||||
// 后续设计图到位后直接替换成正式 UI 图片资源即可。
|
||||
'🎮',
|
||||
style: TextStyle(fontSize: 22.sp),
|
||||
),
|
||||
@ -50,171 +46,15 @@ class RoomGameEntryButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class RoomGameBottomSheet extends StatelessWidget {
|
||||
const RoomGameBottomSheet({super.key});
|
||||
const RoomGameBottomSheet({
|
||||
super.key,
|
||||
required this.roomContext,
|
||||
});
|
||||
|
||||
static const int _itemsPerRow = 5;
|
||||
|
||||
/// 静态占位数据,后续接入真实接口后直接替换这里的数据源即可。
|
||||
static const List<_RoomGameItem> _mockGames = [
|
||||
_RoomGameItem(name: 'Ludo', icon: Icons.casino_outlined),
|
||||
_RoomGameItem(name: 'Dice', icon: Icons.casino),
|
||||
_RoomGameItem(name: 'UNO', icon: Icons.style_outlined),
|
||||
_RoomGameItem(name: 'Quiz', icon: Icons.psychology_outlined),
|
||||
_RoomGameItem(name: 'Race', icon: Icons.sports_motorsports_outlined),
|
||||
_RoomGameItem(name: 'Poker', icon: Icons.style),
|
||||
_RoomGameItem(name: 'Lucky', icon: Icons.auto_awesome_outlined),
|
||||
_RoomGameItem(name: 'Bingo', icon: Icons.grid_on_outlined),
|
||||
_RoomGameItem(name: '2048', icon: Icons.apps_outlined),
|
||||
_RoomGameItem(name: 'Chess', icon: Icons.extension_outlined),
|
||||
_RoomGameItem(name: 'Keno', icon: Icons.confirmation_number_outlined),
|
||||
_RoomGameItem(name: 'Cards', icon: Icons.view_carousel_outlined),
|
||||
_RoomGameItem(name: 'Darts', icon: Icons.gps_fixed),
|
||||
_RoomGameItem(name: 'Puzzle', icon: Icons.interests_outlined),
|
||||
_RoomGameItem(name: 'Slots', icon: Icons.local_activity_outlined),
|
||||
_RoomGameItem(name: 'Spin', icon: Icons.blur_circular_outlined),
|
||||
_RoomGameItem(name: 'Snake', icon: Icons.route_outlined),
|
||||
_RoomGameItem(name: 'Party', icon: Icons.celebration_outlined),
|
||||
_RoomGameItem(name: 'Hero', icon: Icons.shield_outlined),
|
||||
_RoomGameItem(name: 'Arena', icon: Icons.sports_esports_outlined),
|
||||
];
|
||||
final BuildContext roomContext;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rowCount = (_mockGames.length / _itemsPerRow).ceil();
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(18.w),
|
||||
topRight: Radius.circular(18.w),
|
||||
),
|
||||
child: BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16),
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: ScreenUtil().screenHeight / 3,
|
||||
color: const Color(0xff09372E).withValues(alpha: 0.88),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 10.w),
|
||||
Container(
|
||||
width: 34.w,
|
||||
height: 4.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.28),
|
||||
borderRadius: BorderRadius.circular(99.w),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 14.w, 16.w, 10.w),
|
||||
child: Row(
|
||||
children: [
|
||||
text(
|
||||
'Games',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
const Spacer(),
|
||||
text(
|
||||
'Static mock data',
|
||||
fontSize: 11,
|
||||
textColor: Colors.white70,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.fromLTRB(12.w, 0, 12.w, 20.w),
|
||||
itemCount: rowCount,
|
||||
itemBuilder: (context, rowIndex) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 12.w),
|
||||
child: Row(
|
||||
children: List.generate(_itemsPerRow, (columnIndex) {
|
||||
final itemIndex =
|
||||
rowIndex * _itemsPerRow + columnIndex;
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child:
|
||||
itemIndex < _mockGames.length
|
||||
? _RoomGameTile(
|
||||
item: _mockGames[itemIndex],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return RoomGameListSheet(roomContext: roomContext);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomGameTile extends StatelessWidget {
|
||||
const _RoomGameTile({required this.item});
|
||||
|
||||
final _RoomGameItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
const Color(0xff18F2B1).withValues(alpha: 0.26),
|
||||
Colors.white.withValues(alpha: 0.08),
|
||||
],
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
// 当前先用系统 Icon 占位,后续会替换成具体游戏 UI 图标/图片资源。
|
||||
item.icon,
|
||||
size: 22.w,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
text(
|
||||
item.name,
|
||||
fontSize: 11,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomGameItem {
|
||||
const _RoomGameItem({required this.name, required this.icon});
|
||||
|
||||
final String name;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
76
pubspec.lock
76
pubspec.lock
@ -534,13 +534,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
flutter_foreground_task:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "local_packages/flutter_foreground_task-9.1.0"
|
||||
relative: true
|
||||
source: path
|
||||
version: "9.1.0"
|
||||
flutter_image_compress:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -772,14 +765,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
id3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: id3
|
||||
sha256: "24176a6e08db6297c8450079e94569cd8387f913c817e5e3d862be7dc191e0b8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -791,10 +776,9 @@ packages:
|
||||
image_cropper:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_cropper
|
||||
sha256: f4bad5ed2dfff5a7ce0dfbad545b46a945c702bb6182a921488ef01ba7693111
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "local_packages/image_cropper-5.0.1-patched"
|
||||
relative: true
|
||||
source: path
|
||||
version: "5.0.1"
|
||||
image_cropper_for_web:
|
||||
dependency: transitive
|
||||
@ -996,14 +980,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
loading_indicator_view_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: loading_indicator_view_plus
|
||||
sha256: "23ad380dd677e8e0182f679f29e1221269ad66b9a1a2d70b19e716ee4723c99e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1092,44 +1068,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
on_audio_query:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "local_packages/on_audio_query-2.9.0"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.9.0"
|
||||
on_audio_query_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "local_packages/on_audio_query_android-1.1.0"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.0"
|
||||
on_audio_query_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: on_audio_query_ios
|
||||
sha256: "9b3efa39a656fa3720980e3c6a1f55b7257d0032a45ffeb3f70eaa2c7f10f929"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
on_audio_query_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: on_audio_query_platform_interface
|
||||
sha256: c23e019a31bd0774828476e428fd33b0dd1d82c9d4791dba80429358fc65dcd3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
on_audio_query_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: on_audio_query_web
|
||||
sha256: "990efa52d879e6caffa97f24b34acd9caa1ce2c4c4cb873fe5a899a9b1af02c7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1455,14 +1393,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
social_sharing_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: social_sharing_plus
|
||||
sha256: d0bd9dc3358cf66a3aac7cce549902360f30bccf44cf6090a708d97cc54ca854
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
13
pubspec.yaml
13
pubspec.yaml
@ -148,12 +148,13 @@ flutter:
|
||||
# - family: MyCustomFont
|
||||
# fonts:
|
||||
# - asset: fonts/AAA-bold.ttf
|
||||
assets:
|
||||
# 共享资源
|
||||
# 应用资源路径
|
||||
- assets/
|
||||
- assets/l10n/
|
||||
- sc_images/splash/
|
||||
assets:
|
||||
# 共享资源
|
||||
# 应用资源路径
|
||||
- assets/
|
||||
- assets/debug/
|
||||
- assets/l10n/
|
||||
- sc_images/splash/
|
||||
- sc_images/login/
|
||||
- sc_images/general/
|
||||
- sc_images/index/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user