import 'dart:async'; import 'dart:convert'; import 'dart:io'; 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 createState() => _BaishunGamePageState(); } class _BaishunGamePageState extends State { 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 _primeBridgeHandshake(); }, onWebResourceError: (WebResourceError error) { if (!mounted) { return; } setState(() { _errorMessage = error.description; _isLoading = false; }); }, ), ); unawaited(_loadGameEntry()); } Future _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'); } final html = await _fetchRemoteEntryHtml(uri); final injectedHtml = BaishunJsBridge.injectBootstrapHtml( html: html, entryUri: uri, config: widget.launchModel.bridgeConfig, ); await _controller.loadHtmlString(injectedHtml, baseUrl: uri.toString()); } catch (error) { if (!mounted) { return; } setState(() { _errorMessage = error.toString(); _isLoading = false; }); } } Future _fetchRemoteEntryHtml(Uri uri) async { final client = HttpClient(); client.userAgent = 'YumiBaishunBridge/1.0'; try { final request = await client.getUrl(uri); final response = await request.close(); if (response.statusCode < 200 || response.statusCode >= 300) { throw HttpException( 'Failed to load remote entry html (${response.statusCode})', uri: uri, ); } return response.transform(utf8.decoder).join(); } finally { client.close(force: true); } } Future _injectBridge() async { try { await _controller.runJavaScript(BaishunJsBridge.bootstrapScript()); await _controller.runJavaScript( BaishunJsBridge.buildConfigScript(widget.launchModel.bridgeConfig), ); } catch (_) {} } Future _primeBridgeHandshake() async { await _injectBridge(); for (final delay in [ const Duration(milliseconds: 400), const Duration(milliseconds: 1200), ]) { Future.delayed(delay, () async { if (!mounted || !_isLoading || _errorMessage != null) { return; } await _injectBridge(); }); } } Future _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 _notifyWalletUpdate() async { try { await _controller.runJavaScript( BaishunJsBridge.buildWalletUpdateScript(), ); } catch (_) {} } Future _reload() async { if (!mounted) { return; } setState(() { _errorMessage = null; _isLoading = true; }); await _loadGameEntry(); } Future _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 IgnorePointer( ignoring: true, child: 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'), ), ], ), ], ), ), ); } }