游戏1.0提交
This commit is contained in:
parent
73f1cf1199
commit
80e9f3a7d0
@ -1,25 +1,47 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.google.gms.google-services") version "4.4.3" apply false
|
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 {
|
buildscript {
|
||||||
repositories {
|
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()
|
google()
|
||||||
mavenCentral()
|
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 {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
// 使用阿里云镜像加速
|
addMirrorRepos()
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
google()
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
mavenCentral()
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
|
}
|
||||||
// 备用官方仓库
|
}
|
||||||
|
|
||||||
|
gradle.beforeProject {
|
||||||
|
buildscript.repositories.apply {
|
||||||
|
addMirrorRepos()
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories.apply {
|
||||||
|
addMirrorRepos()
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
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.useAndroidX=true
|
||||||
android.enableJetifier=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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -51,7 +51,8 @@ Future<bool> _isCurrentSessionStillValid(DioException e) async {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final verificationHeaders = Map<String, dynamic>.from(e.requestOptions.headers)
|
final verificationHeaders =
|
||||||
|
Map<String, dynamic>.from(e.requestOptions.headers)
|
||||||
..remove("Content-Length")
|
..remove("Content-Length")
|
||||||
..remove("content-length");
|
..remove("content-length");
|
||||||
|
|
||||||
@ -105,6 +106,8 @@ Future<bool> _isCurrentSessionStillValid(DioException e) async {
|
|||||||
|
|
||||||
class BaseNetworkClient {
|
class BaseNetworkClient {
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
|
static const String silentErrorToastKey = 'silentErrorToast';
|
||||||
|
static const String baseUrlOverrideKey = 'baseUrlOverride';
|
||||||
|
|
||||||
BaseNetworkClient() : dio = Dio() {
|
BaseNetworkClient() : dio = Dio() {
|
||||||
_confD();
|
_confD();
|
||||||
@ -126,6 +129,7 @@ class BaseNetworkClient {
|
|||||||
Future<T> get<T>(
|
Future<T> get<T>(
|
||||||
String path, {
|
String path, {
|
||||||
Map<String, dynamic>? queryParams,
|
Map<String, dynamic>? queryParams,
|
||||||
|
Map<String, dynamic>? extra,
|
||||||
required T Function(dynamic) fromJson,
|
required T Function(dynamic) fromJson,
|
||||||
CancelToken? cancelToken,
|
CancelToken? cancelToken,
|
||||||
ProgressCallback? onSendProgress,
|
ProgressCallback? onSendProgress,
|
||||||
@ -135,6 +139,7 @@ class BaseNetworkClient {
|
|||||||
path,
|
path,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
|
extra: extra,
|
||||||
fromJson: fromJson,
|
fromJson: fromJson,
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
@ -147,6 +152,7 @@ class BaseNetworkClient {
|
|||||||
String path, {
|
String path, {
|
||||||
dynamic data,
|
dynamic data,
|
||||||
Map<String, dynamic>? queryParams,
|
Map<String, dynamic>? queryParams,
|
||||||
|
Map<String, dynamic>? extra,
|
||||||
required T Function(dynamic) fromJson,
|
required T Function(dynamic) fromJson,
|
||||||
CancelToken? cancelToken,
|
CancelToken? cancelToken,
|
||||||
ProgressCallback? onSendProgress,
|
ProgressCallback? onSendProgress,
|
||||||
@ -157,6 +163,7 @@ class BaseNetworkClient {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: data,
|
data: data,
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
|
extra: extra,
|
||||||
fromJson: fromJson,
|
fromJson: fromJson,
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
@ -169,6 +176,7 @@ class BaseNetworkClient {
|
|||||||
String path, {
|
String path, {
|
||||||
dynamic data,
|
dynamic data,
|
||||||
Map<String, dynamic>? queryParams,
|
Map<String, dynamic>? queryParams,
|
||||||
|
Map<String, dynamic>? extra,
|
||||||
required T Function(dynamic) fromJson,
|
required T Function(dynamic) fromJson,
|
||||||
CancelToken? cancelToken,
|
CancelToken? cancelToken,
|
||||||
ProgressCallback? onSendProgress,
|
ProgressCallback? onSendProgress,
|
||||||
@ -179,6 +187,7 @@ class BaseNetworkClient {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: data,
|
data: data,
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
|
extra: extra,
|
||||||
fromJson: fromJson,
|
fromJson: fromJson,
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
@ -191,6 +200,7 @@ class BaseNetworkClient {
|
|||||||
String path, {
|
String path, {
|
||||||
dynamic data,
|
dynamic data,
|
||||||
Map<String, dynamic>? queryParams,
|
Map<String, dynamic>? queryParams,
|
||||||
|
Map<String, dynamic>? extra,
|
||||||
required T Function(dynamic) fromJson,
|
required T Function(dynamic) fromJson,
|
||||||
CancelToken? cancelToken,
|
CancelToken? cancelToken,
|
||||||
ProgressCallback? onSendProgress,
|
ProgressCallback? onSendProgress,
|
||||||
@ -201,6 +211,7 @@ class BaseNetworkClient {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
data: data,
|
data: data,
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
|
extra: extra,
|
||||||
fromJson: fromJson,
|
fromJson: fromJson,
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
@ -214,6 +225,7 @@ class BaseNetworkClient {
|
|||||||
required String method,
|
required String method,
|
||||||
dynamic data,
|
dynamic data,
|
||||||
Map<String, dynamic>? queryParams,
|
Map<String, dynamic>? queryParams,
|
||||||
|
Map<String, dynamic>? extra,
|
||||||
CancelToken? cancelToken,
|
CancelToken? cancelToken,
|
||||||
ProgressCallback? onSendProgress,
|
ProgressCallback? onSendProgress,
|
||||||
ProgressCallback? onReceiveProgress,
|
ProgressCallback? onReceiveProgress,
|
||||||
@ -224,7 +236,7 @@ class BaseNetworkClient {
|
|||||||
path,
|
path,
|
||||||
data: data,
|
data: data,
|
||||||
queryParameters: queryParams,
|
queryParameters: queryParams,
|
||||||
options: Options(method: method),
|
options: Options(method: method, extra: extra),
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
onReceiveProgress: onReceiveProgress,
|
onReceiveProgress: onReceiveProgress,
|
||||||
@ -267,6 +279,8 @@ class BaseNetworkClient {
|
|||||||
// 错误处理
|
// 错误处理
|
||||||
Future<DioException> _hdlErr(DioException e) async {
|
Future<DioException> _hdlErr(DioException e) async {
|
||||||
SCLoadingManager.hide();
|
SCLoadingManager.hide();
|
||||||
|
final bool silentErrorToast =
|
||||||
|
e.requestOptions.extra[BaseNetworkClient.silentErrorToastKey] == true;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case DioExceptionType.connectionTimeout:
|
case DioExceptionType.connectionTimeout:
|
||||||
return DioException(requestOptions: e.requestOptions, error: '连接超时');
|
return DioException(requestOptions: e.requestOptions, error: '连接超时');
|
||||||
@ -305,9 +319,11 @@ class BaseNetworkClient {
|
|||||||
SCRoomUtils.goRecharge(context);
|
SCRoomUtils.goRecharge(context);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (!silentErrorToast) {
|
||||||
SCTts.show(errorMsg);
|
SCTts.show(errorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return DioException(
|
return DioException(
|
||||||
requestOptions: e.requestOptions,
|
requestOptions: e.requestOptions,
|
||||||
response: e.response,
|
response: e.response,
|
||||||
|
|||||||
@ -304,7 +304,15 @@ class ApiInterceptor extends Interceptor {
|
|||||||
RequestInterceptorHandler handler,
|
RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
// 确保使用最新的 baseUrl
|
// 确保使用最新的 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);
|
options.path = _resolveRequestPath(options.baseUrl, options.path);
|
||||||
|
|
||||||
String token = AccountStorage().getToken();
|
String token = AccountStorage().getToken();
|
||||||
@ -499,17 +507,14 @@ class TimeOutInterceptor extends Interceptor {
|
|||||||
final int timeoutSeconds = _resolveTimeoutSeconds(options);
|
final int timeoutSeconds = _resolveTimeoutSeconds(options);
|
||||||
|
|
||||||
// 默认 15 秒,上传等特殊请求可通过 extra 覆盖。
|
// 默认 15 秒,上传等特殊请求可通过 extra 覆盖。
|
||||||
_timers[requestKey] = Timer(
|
_timers[requestKey] = Timer(Duration(seconds: timeoutSeconds), () {
|
||||||
Duration(seconds: timeoutSeconds),
|
|
||||||
() {
|
|
||||||
SCLoadingManager.hide();
|
SCLoadingManager.hide();
|
||||||
if (!cancelToken.isCancelled) {
|
if (!cancelToken.isCancelled) {
|
||||||
cancelToken.cancel('请求超时(${timeoutSeconds}秒)');
|
cancelToken.cancel('请求超时(${timeoutSeconds}秒)');
|
||||||
// 从映射中移除计时器
|
// 从映射中移除计时器
|
||||||
_timers.remove(requestKey);
|
_timers.remove(requestKey);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.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/shared/tools/sc_lk_dialog_util.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.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 {
|
class RoomGameEntryButton extends StatelessWidget {
|
||||||
const RoomGameEntryButton({super.key});
|
const RoomGameEntryButton({super.key});
|
||||||
@ -15,7 +13,7 @@ class RoomGameEntryButton extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
showBottomInBottomDialog(
|
showBottomInBottomDialog(
|
||||||
context,
|
context,
|
||||||
const RoomGameBottomSheet(),
|
RoomGameBottomSheet(roomContext: context),
|
||||||
barrierColor: Colors.black54,
|
barrierColor: Colors.black54,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -39,8 +37,6 @@ class RoomGameEntryButton extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
// 当前先用代码绘制的 `🎮` 作为语言房游戏入口占位,
|
|
||||||
// 后续设计图到位后直接替换成正式 UI 图片资源即可。
|
|
||||||
'🎮',
|
'🎮',
|
||||||
style: TextStyle(fontSize: 22.sp),
|
style: TextStyle(fontSize: 22.sp),
|
||||||
),
|
),
|
||||||
@ -50,171 +46,15 @@ class RoomGameEntryButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RoomGameBottomSheet extends StatelessWidget {
|
class RoomGameBottomSheet extends StatelessWidget {
|
||||||
const RoomGameBottomSheet({super.key});
|
const RoomGameBottomSheet({
|
||||||
|
super.key,
|
||||||
|
required this.roomContext,
|
||||||
|
});
|
||||||
|
|
||||||
static const int _itemsPerRow = 5;
|
final BuildContext roomContext;
|
||||||
|
|
||||||
/// 静态占位数据,后续接入真实接口后直接替换这里的数据源即可。
|
|
||||||
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),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final rowCount = (_mockGames.length / _itemsPerRow).ceil();
|
return RoomGameListSheet(roomContext: roomContext);
|
||||||
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
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:
|
flutter_image_compress:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -772,14 +765,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
id3:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: id3
|
|
||||||
sha256: "24176a6e08db6297c8450079e94569cd8387f913c817e5e3d862be7dc191e0b8"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -791,10 +776,9 @@ packages:
|
|||||||
image_cropper:
|
image_cropper:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_cropper
|
path: "local_packages/image_cropper-5.0.1-patched"
|
||||||
sha256: f4bad5ed2dfff5a7ce0dfbad545b46a945c702bb6182a921488ef01ba7693111
|
relative: true
|
||||||
url: "https://pub.dev"
|
source: path
|
||||||
source: hosted
|
|
||||||
version: "5.0.1"
|
version: "5.0.1"
|
||||||
image_cropper_for_web:
|
image_cropper_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -996,14 +980,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
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:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1092,44 +1068,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
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:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1455,14 +1393,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -152,6 +152,7 @@ flutter:
|
|||||||
# 共享资源
|
# 共享资源
|
||||||
# 应用资源路径
|
# 应用资源路径
|
||||||
- assets/
|
- assets/
|
||||||
|
- assets/debug/
|
||||||
- assets/l10n/
|
- assets/l10n/
|
||||||
- sc_images/splash/
|
- sc_images/splash/
|
||||||
- sc_images/login/
|
- sc_images/login/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user