修复
This commit is contained in:
parent
3872dc03b1
commit
b5bea97c2a
303
lib/modules/room_game/bridge/hotgame_js_bridge.dart
Normal file
303
lib/modules/room_game/bridge/hotgame_js_bridge.dart
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class HotgameBridgeActions {
|
||||||
|
static const String recharge = 'recharge';
|
||||||
|
static const String quit = 'quit';
|
||||||
|
static const String loadComplete = 'loadComplete';
|
||||||
|
static const String debugLog = 'debugLog';
|
||||||
|
|
||||||
|
static String normalize(String action) {
|
||||||
|
final trimmed = action.trim();
|
||||||
|
final lower = trimmed.toLowerCase();
|
||||||
|
switch (lower) {
|
||||||
|
case 'recharge':
|
||||||
|
case 'pay':
|
||||||
|
case 'gamepay':
|
||||||
|
case 'gamerecharge':
|
||||||
|
case 'insufficient':
|
||||||
|
case 'insufficientbalance':
|
||||||
|
return recharge;
|
||||||
|
case 'quit':
|
||||||
|
case 'exit':
|
||||||
|
case 'close':
|
||||||
|
case 'closegame':
|
||||||
|
case 'destroy':
|
||||||
|
return quit;
|
||||||
|
case 'loadcomplete':
|
||||||
|
case 'gameloaded':
|
||||||
|
case 'loaded':
|
||||||
|
case 'ready':
|
||||||
|
return loadComplete;
|
||||||
|
case 'debuglog':
|
||||||
|
return debugLog;
|
||||||
|
default:
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HotgameBridgeMessage {
|
||||||
|
const HotgameBridgeMessage({
|
||||||
|
required this.action,
|
||||||
|
this.payload = const <String, dynamic>{},
|
||||||
|
});
|
||||||
|
|
||||||
|
final String action;
|
||||||
|
final Map<String, dynamic> payload;
|
||||||
|
|
||||||
|
static HotgameBridgeMessage fromAction(String action, String rawMessage) {
|
||||||
|
return HotgameBridgeMessage(
|
||||||
|
action: HotgameBridgeActions.normalize(action),
|
||||||
|
payload: _parsePayload(rawMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HotgameBridgeMessage parse(String rawMessage) {
|
||||||
|
try {
|
||||||
|
final dynamic decoded = jsonDecode(rawMessage);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
return HotgameBridgeMessage(
|
||||||
|
action: HotgameBridgeActions.normalize(
|
||||||
|
(decoded['action'] ??
|
||||||
|
decoded['cmd'] ??
|
||||||
|
decoded['event'] ??
|
||||||
|
decoded['method'] ??
|
||||||
|
decoded['type'] ??
|
||||||
|
'')
|
||||||
|
.toString(),
|
||||||
|
),
|
||||||
|
payload: _parsePayload(
|
||||||
|
decoded['payload'] ?? decoded['data'] ?? decoded['params'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return HotgameBridgeMessage(
|
||||||
|
action: HotgameBridgeActions.normalize(rawMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic> _parsePayload(dynamic rawPayload) {
|
||||||
|
if (rawPayload is Map<String, dynamic>) {
|
||||||
|
return rawPayload;
|
||||||
|
}
|
||||||
|
if (rawPayload is String) {
|
||||||
|
final trimmed = rawPayload.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return const <String, dynamic>{};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final dynamic decoded = jsonDecode(trimmed);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return <String, dynamic>{'raw': trimmed};
|
||||||
|
}
|
||||||
|
return const <String, dynamic>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HotgameJsBridge {
|
||||||
|
static const String channelName = 'HotgameBridgeChannel';
|
||||||
|
|
||||||
|
static String bootstrapScript({Map<String, dynamic>? launchPayload}) {
|
||||||
|
final safeLaunchPayload = jsonEncode(
|
||||||
|
launchPayload ?? const <String, dynamic>{},
|
||||||
|
);
|
||||||
|
return '''
|
||||||
|
(function() {
|
||||||
|
const launchPayload = $safeLaunchPayload;
|
||||||
|
|
||||||
|
function toPayload(input) {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(input);
|
||||||
|
} catch (_) {
|
||||||
|
return input.trim() ? { raw: input.trim() } : {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (input && typeof input === 'object') {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyForLog(value) {
|
||||||
|
if (value == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.length > 320 ? value.slice(0, 320) + '...' : value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const text = JSON.stringify(value);
|
||||||
|
return text.length > 320 ? text.slice(0, 320) + '...' : text;
|
||||||
|
} catch (_) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postMessage(action, input) {
|
||||||
|
const payload = toPayload(input);
|
||||||
|
$channelName.postMessage(JSON.stringify({
|
||||||
|
action: action,
|
||||||
|
payload: payload
|
||||||
|
}));
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function postDebug(tag, payload) {
|
||||||
|
postMessage('${HotgameBridgeActions.debugLog}', {
|
||||||
|
tag: tag,
|
||||||
|
message: stringifyForLog(payload)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchLaunchPayload(payload) {
|
||||||
|
const normalizedPayload =
|
||||||
|
payload && typeof payload === 'object' ? payload : {};
|
||||||
|
const launchConfig =
|
||||||
|
normalizedPayload.launchConfig &&
|
||||||
|
typeof normalizedPayload.launchConfig === 'object'
|
||||||
|
? normalizedPayload.launchConfig
|
||||||
|
: {};
|
||||||
|
const entry =
|
||||||
|
normalizedPayload.entry && typeof normalizedPayload.entry === 'object'
|
||||||
|
? normalizedPayload.entry
|
||||||
|
: {};
|
||||||
|
window.__hotgameLaunchPayload = normalizedPayload;
|
||||||
|
window.hotgameLaunchPayload = normalizedPayload;
|
||||||
|
window.__hotgameLaunchConfig = launchConfig;
|
||||||
|
window.hotgameLaunchConfig = launchConfig;
|
||||||
|
window.__hotgameLaunchEntry = entry;
|
||||||
|
window.hotgameLaunchEntry = entry;
|
||||||
|
if (typeof window.onHotgameLaunchConfig === 'function') {
|
||||||
|
window.onHotgameLaunchConfig(launchConfig, normalizedPayload);
|
||||||
|
}
|
||||||
|
if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('hotgameLaunchConfig', { detail: launchConfig })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installPostMessageObject(name, action) {
|
||||||
|
const existing = window[name];
|
||||||
|
if (!existing || typeof existing.postMessage !== 'function') {
|
||||||
|
window[name] = {
|
||||||
|
postMessage: function(body) {
|
||||||
|
return postMessage(action, body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installWebkitHandler(name, action) {
|
||||||
|
window.webkit = window.webkit || {};
|
||||||
|
window.webkit.messageHandlers = window.webkit.messageHandlers || {};
|
||||||
|
if (!window.webkit.messageHandlers[name] ||
|
||||||
|
typeof window.webkit.messageHandlers[name].postMessage !== 'function') {
|
||||||
|
window.webkit.messageHandlers[name] = {
|
||||||
|
postMessage: function(body) {
|
||||||
|
return postMessage(action, body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installJsBridgeFunction(name, action) {
|
||||||
|
window.JsBridge = window.JsBridge || {};
|
||||||
|
window.JsBridge[name] = function(body) {
|
||||||
|
return postMessage(action, body || {});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.__hotgameBridgeReady) {
|
||||||
|
window.__hotgameBridgeReady = true;
|
||||||
|
installJsBridgeFunction('recharge', '${HotgameBridgeActions.recharge}');
|
||||||
|
installJsBridgeFunction('quit', '${HotgameBridgeActions.quit}');
|
||||||
|
installJsBridgeFunction('exit', '${HotgameBridgeActions.quit}');
|
||||||
|
installJsBridgeFunction('closeGame', '${HotgameBridgeActions.quit}');
|
||||||
|
installPostMessageObject('recharge', '${HotgameBridgeActions.recharge}');
|
||||||
|
installPostMessageObject('quit', '${HotgameBridgeActions.quit}');
|
||||||
|
installPostMessageObject('loadComplete', '${HotgameBridgeActions.loadComplete}');
|
||||||
|
installWebkitHandler('recharge', '${HotgameBridgeActions.recharge}');
|
||||||
|
installWebkitHandler('quit', '${HotgameBridgeActions.quit}');
|
||||||
|
installWebkitHandler('loadComplete', '${HotgameBridgeActions.loadComplete}');
|
||||||
|
window.NativeBridge = window.NativeBridge || {};
|
||||||
|
window.NativeBridge.recharge = function(body) {
|
||||||
|
return postMessage('${HotgameBridgeActions.recharge}', body || {});
|
||||||
|
};
|
||||||
|
window.NativeBridge.quit = function(body) {
|
||||||
|
return postMessage('${HotgameBridgeActions.quit}', body || {});
|
||||||
|
};
|
||||||
|
window.NativeBridge.closeGame = function(body) {
|
||||||
|
return postMessage('${HotgameBridgeActions.quit}', body || {});
|
||||||
|
};
|
||||||
|
window.NativeBridge.rechargeSuccess = function(body) {
|
||||||
|
return notifyRechargeSuccess(body || {});
|
||||||
|
};
|
||||||
|
window.__hotgameBridgeDebug = postDebug;
|
||||||
|
postDebug('bridge.ready', {
|
||||||
|
href: window.location && window.location.href,
|
||||||
|
hasJsBridge: !!window.JsBridge,
|
||||||
|
hasRechargeSuccess: typeof window.rechargeSuccess === 'function'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyRechargeSuccess(body) {
|
||||||
|
const payload = body || {};
|
||||||
|
let notified = false;
|
||||||
|
if (typeof window.rechargeSuccess === 'function') {
|
||||||
|
window.rechargeSuccess();
|
||||||
|
notified = true;
|
||||||
|
}
|
||||||
|
if (typeof window.onRechargeSuccess === 'function') {
|
||||||
|
window.onRechargeSuccess(payload);
|
||||||
|
notified = true;
|
||||||
|
}
|
||||||
|
if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('rechargeSuccess', { detail: payload })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return notified;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__hotgameNotifyRechargeSuccess = notifyRechargeSuccess;
|
||||||
|
dispatchLaunchPayload(launchPayload);
|
||||||
|
})();
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String buildRechargeSuccessScript({Map<String, dynamic>? payload}) {
|
||||||
|
final safePayload = jsonEncode(
|
||||||
|
payload ??
|
||||||
|
<String, dynamic>{'timestamp': DateTime.now().millisecondsSinceEpoch},
|
||||||
|
);
|
||||||
|
return '''
|
||||||
|
(function() {
|
||||||
|
const payload = $safePayload;
|
||||||
|
if (typeof window.__hotgameBridgeDebug === 'function') {
|
||||||
|
window.__hotgameBridgeDebug('recharge.success', payload);
|
||||||
|
}
|
||||||
|
if (typeof window.__hotgameNotifyRechargeSuccess === 'function') {
|
||||||
|
window.__hotgameNotifyRechargeSuccess(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window.rechargeSuccess === 'function') {
|
||||||
|
window.rechargeSuccess();
|
||||||
|
}
|
||||||
|
if (typeof window.onRechargeSuccess === 'function') {
|
||||||
|
window.onRechargeSuccess(payload);
|
||||||
|
}
|
||||||
|
if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('rechargeSuccess', { detail: payload })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -127,6 +127,10 @@ class RoomGameListItemModel {
|
|||||||
bool get isLeader =>
|
bool get isLeader =>
|
||||||
provider.toUpperCase() == 'LEADER' || gameType.toUpperCase() == 'LEADER';
|
provider.toUpperCase() == 'LEADER' || gameType.toUpperCase() == 'LEADER';
|
||||||
|
|
||||||
|
bool get isHotgame =>
|
||||||
|
provider.toUpperCase() == 'HOTGAME' ||
|
||||||
|
gameType.toUpperCase() == 'HOTGAME';
|
||||||
|
|
||||||
factory RoomGameListItemModel.fromJson(Map<String, dynamic> json) {
|
factory RoomGameListItemModel.fromJson(Map<String, dynamic> json) {
|
||||||
return RoomGameListItemModel(
|
return RoomGameListItemModel(
|
||||||
gameId: _asString(json['gameId']),
|
gameId: _asString(json['gameId']),
|
||||||
|
|||||||
640
lib/modules/room_game/views/hotgame_game_page.dart
Normal file
640
lib/modules/room_game/views/hotgame_game_page.dart
Normal file
@ -0,0 +1,640 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.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/hotgame_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/utils/room_game_viewport.dart';
|
||||||
|
import 'package:yumi/modules/room_game/views/baishun_loading_view.dart';
|
||||||
|
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_h5_url_utils.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||||
|
|
||||||
|
class HotgameGamePage extends StatefulWidget {
|
||||||
|
const HotgameGamePage({
|
||||||
|
super.key,
|
||||||
|
required this.roomId,
|
||||||
|
required this.game,
|
||||||
|
required this.launchModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String roomId;
|
||||||
|
final RoomGameListItemModel game;
|
||||||
|
final BaishunLaunchModel launchModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HotgameGamePage> createState() => _HotgameGamePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HotgameGamePageState extends State<HotgameGamePage> {
|
||||||
|
static const double _sevenScreenRatio = 0.72;
|
||||||
|
|
||||||
|
final RoomGameRepository _repository = RoomGameRepository();
|
||||||
|
final Set<Factory<OneSequenceGestureRecognizer>> _webGestureRecognizers =
|
||||||
|
<Factory<OneSequenceGestureRecognizer>>{
|
||||||
|
Factory<OneSequenceGestureRecognizer>(() => EagerGestureRecognizer()),
|
||||||
|
};
|
||||||
|
late final WebViewController _controller;
|
||||||
|
|
||||||
|
Timer? _bridgeBootstrapTimer;
|
||||||
|
Timer? _loadingFallbackTimer;
|
||||||
|
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isClosing = false;
|
||||||
|
bool _didReceiveBridgeMessage = false;
|
||||||
|
bool _didFinishPageLoad = false;
|
||||||
|
bool _didInjectProgressBridge = false;
|
||||||
|
int _bridgeInjectCount = 0;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller =
|
||||||
|
WebViewController()
|
||||||
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
|
..setBackgroundColor(Colors.black)
|
||||||
|
..addJavaScriptChannel(
|
||||||
|
HotgameJsBridge.channelName,
|
||||||
|
onMessageReceived: _handleBridgeMessage,
|
||||||
|
)
|
||||||
|
..addJavaScriptChannel(
|
||||||
|
HotgameBridgeActions.recharge,
|
||||||
|
onMessageReceived:
|
||||||
|
(JavaScriptMessage message) => _handleNamedChannelMessage(
|
||||||
|
HotgameBridgeActions.recharge,
|
||||||
|
message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..addJavaScriptChannel(
|
||||||
|
HotgameBridgeActions.quit,
|
||||||
|
onMessageReceived:
|
||||||
|
(JavaScriptMessage message) => _handleNamedChannelMessage(
|
||||||
|
HotgameBridgeActions.quit,
|
||||||
|
message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..addJavaScriptChannel(
|
||||||
|
HotgameBridgeActions.loadComplete,
|
||||||
|
onMessageReceived:
|
||||||
|
(JavaScriptMessage message) => _handleNamedChannelMessage(
|
||||||
|
HotgameBridgeActions.loadComplete,
|
||||||
|
message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..setNavigationDelegate(
|
||||||
|
NavigationDelegate(
|
||||||
|
onPageStarted: (String url) {
|
||||||
|
_log('page_started url=${_clip(url, 240)}');
|
||||||
|
_prepareForPageLoad();
|
||||||
|
},
|
||||||
|
onPageFinished: (String url) async {
|
||||||
|
_didFinishPageLoad = true;
|
||||||
|
_log('page_finished url=${_clip(url, 240)}');
|
||||||
|
await _injectBridge(reason: 'page_finished');
|
||||||
|
if (!mounted || _errorMessage != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onProgress: (int progress) {
|
||||||
|
if (_didInjectProgressBridge ||
|
||||||
|
progress < 10 ||
|
||||||
|
_errorMessage != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_didInjectProgressBridge = true;
|
||||||
|
unawaited(_injectBridge(reason: 'progress_$progress'));
|
||||||
|
},
|
||||||
|
onWebResourceError: (WebResourceError error) {
|
||||||
|
_log(
|
||||||
|
'web_resource_error code=${error.errorCode} '
|
||||||
|
'type=${error.errorType} desc=${_clip(error.description, 300)}',
|
||||||
|
);
|
||||||
|
_stopBridgeBootstrap(reason: 'web_resource_error');
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = error.description;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_log('init launch=${_stringifyForLog(_buildLaunchSummary())}');
|
||||||
|
unawaited(_loadGameEntry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_stopBridgeBootstrap(reason: 'dispose');
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _prepareForPageLoad() {
|
||||||
|
_didReceiveBridgeMessage = false;
|
||||||
|
_didFinishPageLoad = false;
|
||||||
|
_didInjectProgressBridge = false;
|
||||||
|
_bridgeInjectCount = 0;
|
||||||
|
_stopBridgeBootstrap(reason: 'prepare_page_load');
|
||||||
|
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
|
||||||
|
Timer timer,
|
||||||
|
) {
|
||||||
|
if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) {
|
||||||
|
timer.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unawaited(_injectBridge(reason: 'bootstrap'));
|
||||||
|
if (_didFinishPageLoad && timer.tick >= 12) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_loadingFallbackTimer = Timer(const Duration(seconds: 6), () {
|
||||||
|
if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopBridgeBootstrap({String reason = 'manual'}) {
|
||||||
|
if (_bridgeBootstrapTimer != null || _loadingFallbackTimer != null) {
|
||||||
|
_log('stop_bootstrap reason=$reason');
|
||||||
|
}
|
||||||
|
_bridgeBootstrapTimer?.cancel();
|
||||||
|
_bridgeBootstrapTimer = null;
|
||||||
|
_loadingFallbackTimer?.cancel();
|
||||||
|
_loadingFallbackTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadGameEntry() async {
|
||||||
|
try {
|
||||||
|
_prepareForPageLoad();
|
||||||
|
final entryUrl = widget.launchModel.entry.entryUrl.trim();
|
||||||
|
_log(
|
||||||
|
'load_entry launchMode=${widget.launchModel.entry.launchMode} '
|
||||||
|
'entryUrl=${_clip(entryUrl, 240)}',
|
||||||
|
);
|
||||||
|
if (entryUrl.isEmpty || entryUrl.startsWith('mock://')) {
|
||||||
|
await _controller.loadHtmlString(_buildMockHtml());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final h5Url = SCH5UrlUtils.appendToken(entryUrl);
|
||||||
|
final uri = Uri.tryParse(h5Url);
|
||||||
|
if (uri == null) {
|
||||||
|
throw Exception('Invalid hotgame entry url: $entryUrl');
|
||||||
|
}
|
||||||
|
await _controller.loadRequest(uri);
|
||||||
|
} catch (error) {
|
||||||
|
_log('load_entry_error error=${_clip(error.toString(), 400)}');
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = error.toString();
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _injectBridge({String reason = 'manual'}) async {
|
||||||
|
_bridgeInjectCount += 1;
|
||||||
|
if (reason != 'bootstrap' ||
|
||||||
|
_bridgeInjectCount <= 3 ||
|
||||||
|
_bridgeInjectCount % 5 == 0) {
|
||||||
|
_log('inject_bridge reason=$reason count=$_bridgeInjectCount');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await _controller.runJavaScript(
|
||||||
|
HotgameJsBridge.bootstrapScript(launchPayload: _buildLaunchPayload()),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
_log(
|
||||||
|
'inject_bridge_error reason=$reason error=${_clip(error.toString(), 300)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleBridgeMessage(JavaScriptMessage message) async {
|
||||||
|
_log('channel_message raw=${_clip(message.message, 600)}');
|
||||||
|
final bridgeMessage = HotgameBridgeMessage.parse(message.message);
|
||||||
|
await _dispatchBridgeMessage(bridgeMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleNamedChannelMessage(
|
||||||
|
String action,
|
||||||
|
JavaScriptMessage message,
|
||||||
|
) async {
|
||||||
|
final bridgeMessage = HotgameBridgeMessage.fromAction(
|
||||||
|
action,
|
||||||
|
message.message,
|
||||||
|
);
|
||||||
|
_log('named_channel action=$action raw=${_clip(message.message, 600)}');
|
||||||
|
await _dispatchBridgeMessage(bridgeMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _dispatchBridgeMessage(
|
||||||
|
HotgameBridgeMessage bridgeMessage,
|
||||||
|
) async {
|
||||||
|
if (bridgeMessage.action == HotgameBridgeActions.debugLog) {
|
||||||
|
final tag = bridgeMessage.payload['tag']?.toString().trim();
|
||||||
|
final message = bridgeMessage.payload['message']?.toString() ?? '';
|
||||||
|
_log('h5_debug tag=${tag ?? 'unknown'} message=${_clip(message, 800)}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_didReceiveBridgeMessage = true;
|
||||||
|
_stopBridgeBootstrap(reason: 'bridge_message_${bridgeMessage.action}');
|
||||||
|
_log(
|
||||||
|
'bridge_action action=${bridgeMessage.action} '
|
||||||
|
'payload=${_stringifyForLog(_sanitizeForLog(bridgeMessage.payload))}',
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (bridgeMessage.action) {
|
||||||
|
case HotgameBridgeActions.recharge:
|
||||||
|
await _openRechargeAndNotifyGame();
|
||||||
|
break;
|
||||||
|
case HotgameBridgeActions.quit:
|
||||||
|
await _closeAndExit(reason: 'h5_quit');
|
||||||
|
break;
|
||||||
|
case HotgameBridgeActions.loadComplete:
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_log('bridge_action_unhandled action=${bridgeMessage.action}');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openRechargeAndNotifyGame() async {
|
||||||
|
_log('hotgame_recharge open_wallet');
|
||||||
|
await SCNavigatorUtils.push(context, WalletRoute.recharge);
|
||||||
|
try {
|
||||||
|
await _controller.runJavaScript(
|
||||||
|
HotgameJsBridge.buildRechargeSuccessScript(),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
_log(
|
||||||
|
'recharge_success_notify_error error=${_clip(error.toString(), 300)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reload() async {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = null;
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
await _loadGameEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _closeAndExit({String reason = 'user_exit'}) async {
|
||||||
|
if (_isClosing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isClosing = true;
|
||||||
|
try {
|
||||||
|
final sessionId = widget.launchModel.gameSessionId.trim();
|
||||||
|
if (sessionId.isNotEmpty && !sessionId.startsWith('mock')) {
|
||||||
|
await _repository.closeGame(
|
||||||
|
provider: widget.game.provider,
|
||||||
|
roomId: widget.roomId,
|
||||||
|
gameSessionId: sessionId,
|
||||||
|
reason: reason,
|
||||||
|
params: const <String, dynamic>{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
_log('close_error error=${_clip(error.toString(), 300)}');
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveScreenMode() {
|
||||||
|
return resolveRoomGameScreenMode(
|
||||||
|
widget.game.launchParams,
|
||||||
|
fullScreen: widget.game.fullScreen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _resolveSafeHeight() {
|
||||||
|
if (widget.launchModel.entry.safeHeight > 0) {
|
||||||
|
return widget.launchModel.entry.safeHeight;
|
||||||
|
}
|
||||||
|
if (widget.game.safeHeight > 0) {
|
||||||
|
return widget.game.safeHeight;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calculateBodyHeight(BuildContext context) {
|
||||||
|
final screenMode = _resolveScreenMode();
|
||||||
|
final maxDialogHeight = resolveRoomGameAvailableHeight(
|
||||||
|
context,
|
||||||
|
reservedTop: 12.w,
|
||||||
|
);
|
||||||
|
if (maxDialogHeight <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenMode == 'full') {
|
||||||
|
return maxDialogHeight;
|
||||||
|
}
|
||||||
|
if (screenMode == 'seven') {
|
||||||
|
return maxDialogHeight * _sevenScreenRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clampRoomGameHeight(
|
||||||
|
_calculatePreferredBodyHeight(context),
|
||||||
|
maxDialogHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calculatePreferredBodyHeight(BuildContext context) {
|
||||||
|
final mediaQuery = MediaQuery.maybeOf(context);
|
||||||
|
final screenSize = mediaQuery?.size ?? Size.zero;
|
||||||
|
final safeHeight = _resolveSafeHeight();
|
||||||
|
final fallback = screenSize.width;
|
||||||
|
|
||||||
|
if (safeHeight > 0 && screenSize.width > 0) {
|
||||||
|
final ratio = roomGameDesignWidth / safeHeight;
|
||||||
|
return screenSize.width / ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _log(String message) {}
|
||||||
|
|
||||||
|
String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) {
|
||||||
|
final trimmed = value.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (trimmed.length <= keepStart + keepEnd) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return '${trimmed.substring(0, keepStart)}***${trimmed.substring(trimmed.length - keepEnd)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _sanitizeForLog(Object? value, {String key = ''}) {
|
||||||
|
final lowerKey = key.toLowerCase();
|
||||||
|
final shouldMask =
|
||||||
|
lowerKey.contains('code') ||
|
||||||
|
lowerKey.contains('token') ||
|
||||||
|
lowerKey.contains('authorization');
|
||||||
|
if (value is Map) {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
for (final MapEntry<dynamic, dynamic> entry in value.entries) {
|
||||||
|
result[entry.key.toString()] = _sanitizeForLog(
|
||||||
|
entry.value,
|
||||||
|
key: entry.key.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (value is List) {
|
||||||
|
return value.map((Object? item) => _sanitizeForLog(item)).toList();
|
||||||
|
}
|
||||||
|
if (value is String) {
|
||||||
|
final normalized = shouldMask ? _maskValue(value) : value;
|
||||||
|
return _clip(normalized, 600);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _clip(String value, [int limit = 600]) {
|
||||||
|
final trimmed = value.trim();
|
||||||
|
if (trimmed.length <= limit) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return '${trimmed.substring(0, limit)}...';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _stringifyForLog(Object? value) {
|
||||||
|
if (value == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return _clip(jsonEncode(value), 800);
|
||||||
|
} catch (_) {
|
||||||
|
return _clip(value.toString(), 800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildLaunchPayload() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'provider': widget.launchModel.provider,
|
||||||
|
'gameId': widget.launchModel.gameId,
|
||||||
|
'providerGameId': widget.launchModel.providerGameId,
|
||||||
|
'gameSessionId': widget.launchModel.gameSessionId,
|
||||||
|
'entry': widget.launchModel.entry.toJson(),
|
||||||
|
'launchConfig': widget.launchModel.launchConfig.toJson(),
|
||||||
|
'roomState': widget.launchModel.roomState.toJson(),
|
||||||
|
'game': <String, dynamic>{
|
||||||
|
'gameId': widget.game.gameId,
|
||||||
|
'provider': widget.game.provider,
|
||||||
|
'gameType': widget.game.gameType,
|
||||||
|
'name': widget.game.name,
|
||||||
|
'launchMode': widget.game.launchMode,
|
||||||
|
'launchParams': widget.game.launchParams,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildLaunchSummary() {
|
||||||
|
final payload = _buildLaunchPayload();
|
||||||
|
return <String, dynamic>{
|
||||||
|
'roomId': widget.roomId,
|
||||||
|
'provider': widget.game.provider,
|
||||||
|
'gameId': widget.game.gameId,
|
||||||
|
'providerGameId': widget.launchModel.providerGameId,
|
||||||
|
'gameSessionId': widget.launchModel.gameSessionId,
|
||||||
|
'entry': _sanitizeForLog(payload['entry']),
|
||||||
|
'launchConfig': _sanitizeForLog(payload['launchConfig']),
|
||||||
|
'roomState': _sanitizeForLog(payload['roomState']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildMockHtml() {
|
||||||
|
return '''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
body { margin: 0; font-family: sans-serif; background: #111827; color: #fff; }
|
||||||
|
.wrap { padding: 24px; }
|
||||||
|
button {
|
||||||
|
margin: 8px 8px 0 0;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#log { margin-top: 16px; white-space: pre-wrap; color: #bfdbfe; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h3>Hotgame Mock</h3>
|
||||||
|
<button onclick="loadComplete.postMessage(null)">loadComplete</button>
|
||||||
|
<button onclick="window.JsBridge.recharge()">JsBridge.recharge()</button>
|
||||||
|
<button onclick="window.JsBridge.quit()">JsBridge.quit()</button>
|
||||||
|
<button onclick="window.webkit.messageHandlers.recharge.postMessage(null)">webkit recharge</button>
|
||||||
|
<button onclick="window.webkit.messageHandlers.quit.postMessage(null)">webkit quit</button>
|
||||||
|
<button onclick="window.rechargeSuccess && window.rechargeSuccess()">rechargeSuccess()</button>
|
||||||
|
<div id="log"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
window.rechargeSuccess = function() {
|
||||||
|
document.getElementById('log').textContent += '\\nrechargeSuccess called';
|
||||||
|
};
|
||||||
|
document.getElementById('log').textContent =
|
||||||
|
'launchConfig=' + JSON.stringify(window.hotgameLaunchConfig || {});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bodyHeight = _calculateBodyHeight(context);
|
||||||
|
final bottomFrameHeight = resolveRoomGameCompatibilityBottomHeight(context);
|
||||||
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||||
|
if (didPop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _closeAndExit();
|
||||||
|
},
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(24.w),
|
||||||
|
topRight: Radius.circular(24.w),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: ScreenUtil().screenWidth,
|
||||||
|
height: bodyHeight + bottomFrameHeight,
|
||||||
|
color: const Color(0xFF081915),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: bottomFrameHeight,
|
||||||
|
child: WebViewWidget(
|
||||||
|
controller: _controller,
|
||||||
|
gestureRecognizers: _webGestureRecognizers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (bottomFrameHeight > 0)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: bottomFrameHeight,
|
||||||
|
child: RoomGameCompatibilityBottomFrame(
|
||||||
|
height: bottomFrameHeight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_errorMessage != null) _buildErrorState(),
|
||||||
|
if (_isLoading && _errorMessage == null)
|
||||||
|
const IgnorePointer(
|
||||||
|
ignoring: true,
|
||||||
|
child: BaishunLoadingView(
|
||||||
|
message: 'Waiting for Hotgame...',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
'Hotgame 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 verify the Hotgame entry url');
|
||||||
|
},
|
||||||
|
child: const Text('Tips'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ 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/data/room_game_repository.dart';
|
||||||
import 'package:yumi/modules/room_game/utils/room_game_viewport.dart';
|
import 'package:yumi/modules/room_game/utils/room_game_viewport.dart';
|
||||||
import 'package:yumi/modules/room_game/views/baishun_game_page.dart';
|
import 'package:yumi/modules/room_game/views/baishun_game_page.dart';
|
||||||
|
import 'package:yumi/modules/room_game/views/hotgame_game_page.dart';
|
||||||
import 'package:yumi/modules/room_game/views/leader_game_page.dart';
|
import 'package:yumi/modules/room_game/views/leader_game_page.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||||
@ -33,8 +34,8 @@ class RoomGameListSheet extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||||
static const String _logPrefix = '[RoomGameLaunch]';
|
|
||||||
static const int _itemsPerRow = 4;
|
static const int _itemsPerRow = 4;
|
||||||
|
static const String _logPrefix = '[RoomGameList]';
|
||||||
static const String _sheetFrameAsset =
|
static const String _sheetFrameAsset =
|
||||||
'sc_images/room/sc_room_game_sheet_frame.png';
|
'sc_images/room/sc_room_game_sheet_frame.png';
|
||||||
static const String _dividerLayerAsset =
|
static const String _dividerLayerAsset =
|
||||||
@ -92,7 +93,9 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
|||||||
String targetGameId,
|
String targetGameId,
|
||||||
) {
|
) {
|
||||||
final supportedItems =
|
final supportedItems =
|
||||||
items.where((item) => item.isBaishun || item.isLeader).toList();
|
items
|
||||||
|
.where((item) => item.isBaishun || item.isLeader || item.isHotgame)
|
||||||
|
.toList();
|
||||||
final candidates = supportedItems.isNotEmpty ? supportedItems : items;
|
final candidates = supportedItems.isNotEmpty ? supportedItems : items;
|
||||||
if (targetGameId == '0') {
|
if (targetGameId == '0') {
|
||||||
return candidates[math.Random().nextInt(candidates.length)];
|
return candidates[math.Random().nextInt(candidates.length)];
|
||||||
@ -119,7 +122,7 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
|||||||
SCTts.show('roomId is empty');
|
SCTts.show('roomId is empty');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!game.isBaishun && !game.isLeader) {
|
if (!game.isBaishun && !game.isLeader && !game.isHotgame) {
|
||||||
SCTts.show('This game provider is not wired yet');
|
SCTts.show('This game provider is not wired yet');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -273,6 +276,13 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
|||||||
launchModel: launchModel,
|
launchModel: launchModel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (game.isHotgame) {
|
||||||
|
return HotgameGamePage(
|
||||||
|
roomId: _roomId,
|
||||||
|
game: game,
|
||||||
|
launchModel: launchModel,
|
||||||
|
);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
|||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
|
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
@ -17,8 +18,11 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
|||||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||||
|
import 'package:yumi/modules/country/country_route.dart';
|
||||||
import '../../../shared/tools/sc_pick_utils.dart';
|
import '../../../shared/tools/sc_pick_utils.dart';
|
||||||
|
import '../../../shared/business_logic/models/res/country_res.dart';
|
||||||
import '../../../shared/business_logic/models/res/login_res.dart';
|
import '../../../shared/business_logic/models/res/login_res.dart';
|
||||||
|
import '../../../shared/business_logic/models/res/sc_user_identity_res.dart';
|
||||||
import '../../../shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart';
|
import '../../../shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart';
|
||||||
|
|
||||||
class EditUserInfoPage2 extends StatefulWidget {
|
class EditUserInfoPage2 extends StatefulWidget {
|
||||||
@ -37,6 +41,9 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
String? bornMonth;
|
String? bornMonth;
|
||||||
String? autograph;
|
String? autograph;
|
||||||
String? hobby;
|
String? hobby;
|
||||||
|
String? countryId;
|
||||||
|
String? countryCode;
|
||||||
|
String? countryName;
|
||||||
String? bornDay;
|
String? bornDay;
|
||||||
String? bornYear;
|
String? bornYear;
|
||||||
String nickName = "";
|
String nickName = "";
|
||||||
@ -53,6 +60,9 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
nickName = currentProfile?.userNickname ?? "";
|
nickName = currentProfile?.userNickname ?? "";
|
||||||
autograph = currentProfile?.autograph ?? "";
|
autograph = currentProfile?.autograph ?? "";
|
||||||
hobby = currentProfile?.hobby ?? "";
|
hobby = currentProfile?.hobby ?? "";
|
||||||
|
countryId = currentProfile?.countryId ?? "";
|
||||||
|
countryCode = currentProfile?.countryCode ?? "";
|
||||||
|
countryName = currentProfile?.countryName ?? "";
|
||||||
birthdayDate =
|
birthdayDate =
|
||||||
_birthdayFromProfile(currentProfile) ??
|
_birthdayFromProfile(currentProfile) ??
|
||||||
_birthdayFromAge(currentProfile?.age);
|
_birthdayFromAge(currentProfile?.age);
|
||||||
@ -63,6 +73,19 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
bornYear = _formatYear(currentProfile?.bornYear ?? birthdayDate?.year);
|
bornYear = _formatYear(currentProfile?.bornYear ?? birthdayDate?.year);
|
||||||
age = currentProfile?.age ?? _ageFromBirthday(birthdayDate);
|
age = currentProfile?.age ?? _ageFromBirthday(birthdayDate);
|
||||||
sex = currentProfile?.userSex;
|
sex = currentProfile?.userSex;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Provider.of<SocialChatUserProfileManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).getUserIdentity();
|
||||||
|
Provider.of<SCAppGeneralManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).fetchCountryList();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _preferNonEmpty(String? primary, String? fallback) {
|
String? _preferNonEmpty(String? primary, String? fallback) {
|
||||||
@ -157,11 +180,42 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
return values.any((value) => !identical(value, _noChange));
|
return values.any((value) => !identical(value, _noChange));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _hasIdentity(SCUserIdentityRes? identity) {
|
||||||
|
if (identity == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (identity.anchor ?? false) ||
|
||||||
|
(identity.bd ?? false) ||
|
||||||
|
(identity.agent ?? false) ||
|
||||||
|
(identity.bdLeader ?? false) ||
|
||||||
|
(identity.freightAgent ?? false) ||
|
||||||
|
(identity.superFreightAgent ?? false) ||
|
||||||
|
(identity.admin ?? false) ||
|
||||||
|
(identity.superAdmin ?? false) ||
|
||||||
|
(identity.manager ?? false) ||
|
||||||
|
(identity.yumiManager ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _canChangeCountry(SCUserIdentityRes? identity) {
|
||||||
|
return !_hasIdentity(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _countryDisplayText() {
|
||||||
|
final displayName = (countryName ?? "").trim();
|
||||||
|
if (displayName.isNotEmpty) {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
return (countryCode ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
void _syncLocalProfileState(SocialChatUserProfile profile) {
|
void _syncLocalProfileState(SocialChatUserProfile profile) {
|
||||||
userCover = _preferUsableAvatar(profile.userAvatar, userCover);
|
userCover = _preferUsableAvatar(profile.userAvatar, userCover);
|
||||||
nickName = profile.userNickname ?? nickName;
|
nickName = profile.userNickname ?? nickName;
|
||||||
autograph = profile.autograph ?? autograph;
|
autograph = profile.autograph ?? autograph;
|
||||||
hobby = profile.hobby ?? hobby;
|
hobby = profile.hobby ?? hobby;
|
||||||
|
countryId = profile.countryId ?? countryId;
|
||||||
|
countryCode = profile.countryCode ?? countryCode;
|
||||||
|
countryName = profile.countryName ?? countryName;
|
||||||
sex = profile.userSex ?? sex;
|
sex = profile.userSex ?? sex;
|
||||||
birthdayDate =
|
birthdayDate =
|
||||||
_birthdayFromProfile(profile) ??
|
_birthdayFromProfile(profile) ??
|
||||||
@ -284,6 +338,12 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
_showInputBioHobby(hobby ?? "", 2);
|
_showInputBioHobby(hobby ?? "", 2);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (_canChangeCountry(ref.userIdentity))
|
||||||
|
_buildItem(
|
||||||
|
"${SCAppLocalizations.of(context)!.countryRegion}:",
|
||||||
|
_countryDisplayText(),
|
||||||
|
_openCountryPicker,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -421,6 +481,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
Object? bornDayValue = _noChange,
|
Object? bornDayValue = _noChange,
|
||||||
Object? hobbyValue = _noChange,
|
Object? hobbyValue = _noChange,
|
||||||
Object? autographValue = _noChange,
|
Object? autographValue = _noChange,
|
||||||
|
Object? countryValue = _noChange,
|
||||||
}) async {
|
}) async {
|
||||||
if (!_hasChanges([
|
if (!_hasChanges([
|
||||||
userAvatarValue,
|
userAvatarValue,
|
||||||
@ -432,6 +493,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
bornDayValue,
|
bornDayValue,
|
||||||
hobbyValue,
|
hobbyValue,
|
||||||
autographValue,
|
autographValue,
|
||||||
|
countryValue,
|
||||||
])) {
|
])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -446,6 +508,8 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
_isSubmitting = true;
|
_isSubmitting = true;
|
||||||
SCLoadingManager.show();
|
SCLoadingManager.show();
|
||||||
try {
|
try {
|
||||||
|
final selectedCountry =
|
||||||
|
identical(countryValue, _noChange) ? null : countryValue as Country?;
|
||||||
final updatedProfile = await SCAccountRepository().updateUserInfo(
|
final updatedProfile = await SCAccountRepository().updateUserInfo(
|
||||||
userAvatar:
|
userAvatar:
|
||||||
identical(userAvatarValue, _noChange)
|
identical(userAvatarValue, _noChange)
|
||||||
@ -471,6 +535,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
identical(autographValue, _noChange)
|
identical(autographValue, _noChange)
|
||||||
? null
|
? null
|
||||||
: autographValue as String?,
|
: autographValue as String?,
|
||||||
|
countryId: selectedCountry?.id,
|
||||||
);
|
);
|
||||||
final mergedProfile = updatedProfile.copyWith(
|
final mergedProfile = updatedProfile.copyWith(
|
||||||
userAvatar: _preferUsableAvatar(
|
userAvatar: _preferUsableAvatar(
|
||||||
@ -516,6 +581,21 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
(identical(bornYearValue, _noChange)
|
(identical(bornYearValue, _noChange)
|
||||||
? birthdayDate?.year
|
? birthdayDate?.year
|
||||||
: bornYearValue as num?),
|
: bornYearValue as num?),
|
||||||
|
countryId: _preferNonEmpty(
|
||||||
|
updatedProfile.countryId,
|
||||||
|
selectedCountry?.id,
|
||||||
|
),
|
||||||
|
countryCode: _preferNonEmpty(
|
||||||
|
updatedProfile.countryCode,
|
||||||
|
selectedCountry?.alphaTwo,
|
||||||
|
),
|
||||||
|
countryName: _preferNonEmpty(
|
||||||
|
updatedProfile.countryName,
|
||||||
|
_preferNonEmpty(
|
||||||
|
selectedCountry?.countryName,
|
||||||
|
selectedCountry?.aliasName,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
_syncLocalProfileState(mergedProfile);
|
_syncLocalProfileState(mergedProfile);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
@ -529,13 +609,49 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
|
|||||||
true;
|
true;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful);
|
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful);
|
||||||
} catch (e) {
|
if (selectedCountry != null) {
|
||||||
|
AccountStorage().logout(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// The repository/network layer already shows request errors, so keep the
|
||||||
|
// current edit page state and only release the submit/loading guards here.
|
||||||
} finally {
|
} finally {
|
||||||
_isSubmitting = false;
|
_isSubmitting = false;
|
||||||
SCLoadingManager.hide();
|
SCLoadingManager.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _openCountryPicker() async {
|
||||||
|
final userIdentity =
|
||||||
|
Provider.of<SocialChatUserProfileManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).userIdentity;
|
||||||
|
if (!_canChangeCountry(userIdentity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final generalManager = Provider.of<SCAppGeneralManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
generalManager.clearCountrySelection();
|
||||||
|
await SCNavigatorUtils.push(context, CountryRoute.country, replace: false);
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final selectedCountry = generalManager.selectCountryInfo;
|
||||||
|
if (selectedCountry == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final selectedCountryId = (selectedCountry.id ?? "").trim();
|
||||||
|
if (selectedCountryId.isEmpty ||
|
||||||
|
selectedCountryId == (countryId ?? "").trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submit(countryValue: selectedCountry);
|
||||||
|
}
|
||||||
|
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
void _showSexDialog() {
|
void _showSexDialog() {
|
||||||
showCenterDialog(
|
showCenterDialog(
|
||||||
|
|||||||
62
test/hotgame_bridge_test.dart
Normal file
62
test/hotgame_bridge_test.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:yumi/modules/room_game/bridge/hotgame_js_bridge.dart';
|
||||||
|
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('normalizes documented hotgame recharge and quit bridge actions', () {
|
||||||
|
expect(HotgameBridgeActions.normalize('recharge'), 'recharge');
|
||||||
|
expect(HotgameBridgeActions.normalize('pay'), 'recharge');
|
||||||
|
expect(HotgameBridgeActions.normalize('gameRecharge'), 'recharge');
|
||||||
|
expect(HotgameBridgeActions.normalize('quit'), 'quit');
|
||||||
|
expect(HotgameBridgeActions.normalize('closeGame'), 'quit');
|
||||||
|
expect(HotgameBridgeActions.normalize('destroy'), 'quit');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses hotgame bridge payload maps', () {
|
||||||
|
final message = HotgameBridgeMessage.parse(
|
||||||
|
'{"action":"gameRecharge","payload":{"source":"button"}}',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(message.action, HotgameBridgeActions.recharge);
|
||||||
|
expect(message.payload['source'], 'button');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'bootstrap script contains documented hotgame client bridge methods',
|
||||||
|
() {
|
||||||
|
final script = HotgameJsBridge.bootstrapScript();
|
||||||
|
|
||||||
|
expect(script, contains('window.JsBridge'));
|
||||||
|
expect(script, contains("installJsBridgeFunction('recharge'"));
|
||||||
|
expect(script, contains("installJsBridgeFunction('quit'"));
|
||||||
|
expect(script, contains("installWebkitHandler('recharge'"));
|
||||||
|
expect(script, contains("installWebkitHandler('quit'"));
|
||||||
|
expect(script, contains('window.rechargeSuccess'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('detects hotgame room game list items', () {
|
||||||
|
const item = RoomGameListItemModel(
|
||||||
|
gameId: 'hg_1',
|
||||||
|
provider: 'HOTGAME',
|
||||||
|
gameType: 'HOTGAME',
|
||||||
|
providerGameId: 'Seven7',
|
||||||
|
name: 'Lucky77',
|
||||||
|
cover: '',
|
||||||
|
category: 'CHAT_ROOM',
|
||||||
|
sort: 1,
|
||||||
|
launchMode: 'H5_REMOTE',
|
||||||
|
fullScreen: false,
|
||||||
|
gameMode: 3,
|
||||||
|
safeHeight: 0,
|
||||||
|
orientation: 0,
|
||||||
|
packageVersion: '2026',
|
||||||
|
status: 'ENABLED',
|
||||||
|
launchParams: <String, dynamic>{'screenMode': 'seven'},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.isHotgame, isTrue);
|
||||||
|
expect(item.isBaishun, isFalse);
|
||||||
|
expect(item.isLeader, isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user