diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ce86ffe..cfec910 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -18,13 +18,17 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json
index b279e2f..9983fd5 100644
--- a/assets/l10n/intl_ar.json
+++ b/assets/l10n/intl_ar.json
@@ -147,8 +147,9 @@
"systemAnnouncement": "إعلان النظام",
"doNotClickUnfamiliarTips": "ماتضغطش على الروابط الغير معروفة، حيث قد تكشف معلوماتك الشخصية. متشاركش أبدا بطاقة الهوية أو معلومات البطاقة البنكية ديالك مع أي واحد.",
"atTag": "@تاغ",
- "sayHi2": "قل مرحبا",
- "msgSendRedEnvelopeTips": "سيتم فرض رسوم خدمة بنسبة 10% على المظاريف الحمراء، ولن يحصل المستلمون إلا على 90% من قيمة المظروف. يجب أن يكون مستوى ثروة المرسل أعلى من المستوى 10.",
+ "sayHi2": "قل مرحبا",
+ "roomBottomGreeting": "مرحبًا...",
+ "msgSendRedEnvelopeTips": "سيتم فرض رسوم خدمة بنسبة 10% على المظاريف الحمراء، ولن يحصل المستلمون إلا على 90% من قيمة المظروف. يجب أن يكون مستوى ثروة المرسل أعلى من المستوى 10.",
"reapply": "أعد التقديم",
"cancelRequest": "إلغاء الطلب",
"supporter": "مشجع",
diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json
index e2790f3..6d0fb8a 100644
--- a/assets/l10n/intl_bn.json
+++ b/assets/l10n/intl_bn.json
@@ -187,8 +187,9 @@
"vistors": "ভিজিটর",
"fans": "ফ্যান",
"balanceNotEnough": "সোনা কয়েন ব্যালেন্স অপর্যাপ্ত। আপনি কি রিচার্জ করতে চান?",
- "skip": "{1} স্কিপ করুন",
- "letGoToWatch": "চলুন দেখতে যাই!",
+ "skip": "{1} স্কিপ করুন",
+ "roomBottomGreeting": "হাই...",
+ "letGoToWatch": "চলুন দেখতে যাই!",
"launchedARocket": "রকেট চালু করেছে",
"sendUserId": "ব্যবহারকারী ID পাঠান:{1}",
"giveUpIdentity": "আইডেন্টিটি ছেড়ে দিন",
diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json
index c624327..6d701ee 100644
--- a/assets/l10n/intl_en.json
+++ b/assets/l10n/intl_en.json
@@ -78,8 +78,9 @@
"systemAnnouncement": "System Announcement",
"doNotClickUnfamiliarTips": "Do not click unfamiliar links, as they can expose your personal information. Never share your ID or bank card details with anyone.",
"atTag": "@Tag",
- "sayHi2": "Say Hi",
- "canSendMsgTips": "Both parties need to follow each other before they can send private messages.",
+ "sayHi2": "Say Hi",
+ "roomBottomGreeting": "Hi...",
+ "canSendMsgTips": "Both parties need to follow each other before they can send private messages.",
"msgSendRedEnvelopeTips": "*A 10% service fee will be charged on red envelopes, and recipients will only receive 90% of the red envelope's value. The sender's wealth level must be higher than Level 10.",
"reapply": "Reapply",
"cancelRequest": "Cancel Request",
diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json
index db3e44e..f256bfd 100644
--- a/assets/l10n/intl_tr.json
+++ b/assets/l10n/intl_tr.json
@@ -68,8 +68,9 @@
"systemAnnouncement": "Sistem Duyurusu",
"doNotClickUnfamiliarTips": "Tanımadığınız bağlantılara tıklayın, çünkü bunlar kişisel bilgilerinizi ifşa edebilir. Kimlik numaranızı veya banka kartı detaylarınızı asla kimseyle paylaşmayın.",
"atTag": "@Etiket",
- "sayHi2": "Merhaba De",
- "canSendMsgTips": "Özel mesaj göndermek için her iki tarafın da birbirini takip etmesi gerekir.",
+ "sayHi2": "Merhaba De",
+ "roomBottomGreeting": "Merhaba...",
+ "canSendMsgTips": "Özel mesaj göndermek için her iki tarafın da birbirini takip etmesi gerekir.",
"msgSendRedEnvelopeTips": "*Kırmızı zarflar üzerinde %10 hizmet ücreti kesilecektir ve alıcılar yalnızca kırmızı zarfın değerinin %90'ını alacaktır. Göndericinin servet seviyesi 10. Seviyeden yüksek olmalıdır.",
"reapply": "Tekrar Başvur",
"cancelRequest": "İsteği İptal Et",
diff --git a/lib/modules/index/index_page.dart b/lib/modules/index/index_page.dart
index 28248ab..a5a0d10 100644
--- a/lib/modules/index/index_page.dart
+++ b/lib/modules/index/index_page.dart
@@ -14,6 +14,7 @@ import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
+import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
import '../../shared/tools/sc_heartbeat_utils.dart';
import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
import '../../ui_kit/components/sc_float_ichart.dart';
@@ -181,30 +182,30 @@ class _SCIndexPageState extends State {
_bottomItems.add(
BottomNavigationBarItem(
- icon: Image.asset(
- "sc_images/index/sc_icon_home_no.png",
- width: 35.w,
- height: 35.w,
+ icon: _buildBottomTabIcon(
+ active: false,
+ svgaPath: "sc_images/index/sc_icon_home_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_home_no.png",
),
- activeIcon: Image.asset(
- "sc_images/index/sc_icon_home_en.png",
- width: 35.w,
- height: 35.w,
+ activeIcon: _buildBottomTabIcon(
+ active: true,
+ svgaPath: "sc_images/index/sc_icon_home_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_home_en.png",
),
label: SCAppLocalizations.of(context)!.home,
),
);
_bottomItems.add(
BottomNavigationBarItem(
- icon: Image.asset(
- "sc_images/index/sc_icon_home_no.png",
- width: 35.w,
- height: 35.w,
+ icon: _buildBottomTabIcon(
+ active: false,
+ svgaPath: "sc_images/index/sc_icon_explore_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_explore_no.png",
),
- activeIcon: Image.asset(
- "sc_images/index/sc_icon_home_en.png",
- width: 35.w,
- height: 35.w,
+ activeIcon: _buildBottomTabIcon(
+ active: true,
+ svgaPath: "sc_images/index/sc_icon_explore_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_explore_en.png",
),
label: SCAppLocalizations.of(context)!.explore,
),
@@ -226,16 +227,16 @@ class _SCIndexPageState extends State {
fontWeight: FontWeight.w600,
),
alignment: AlignmentDirectional.topEnd,
- child: Image.asset(
- "sc_images/index/sc_icon_message_no.png",
- width: 35.w,
- height: 35.w,
+ child: _buildBottomTabIcon(
+ active: false,
+ svgaPath: "sc_images/index/sc_icon_message_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_message_no.png",
),
)
- : Image.asset(
- "sc_images/index/sc_icon_message_no.png",
- width: 35.w,
- height: 35.w,
+ : _buildBottomTabIcon(
+ active: false,
+ svgaPath: "sc_images/index/sc_icon_message_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_message_no.png",
);
},
),
@@ -253,16 +254,16 @@ class _SCIndexPageState extends State {
fontWeight: FontWeight.w600,
),
alignment: AlignmentDirectional.topEnd,
- child: Image.asset(
- "sc_images/index/sc_icon_message_en.png",
- width: 35.w,
- height: 35.w,
+ child: _buildBottomTabIcon(
+ active: true,
+ svgaPath: "sc_images/index/sc_icon_message_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_message_en.png",
),
)
- : Image.asset(
- "sc_images/index/sc_icon_message_en.png",
- width: 35.w,
- height: 35.w,
+ : _buildBottomTabIcon(
+ active: true,
+ svgaPath: "sc_images/index/sc_icon_message_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_message_en.png",
);
},
),
@@ -271,18 +272,33 @@ class _SCIndexPageState extends State {
);
_bottomItems.add(
BottomNavigationBarItem(
- icon: Image.asset(
- "sc_images/index/sc_icon_me_no.png",
- width: 35.w,
- height: 35.w,
+ icon: _buildBottomTabIcon(
+ active: false,
+ svgaPath: "sc_images/index/sc_icon_me_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_me_no.png",
),
- activeIcon: Image.asset(
- "sc_images/index/sc_icon_me_en.png",
- width: 35.w,
- height: 35.w,
+ activeIcon: _buildBottomTabIcon(
+ active: true,
+ svgaPath: "sc_images/index/sc_icon_me_anim.svga",
+ fallbackPath: "sc_images/index/sc_icon_me_en.png",
),
label: SCAppLocalizations.of(context)!.me,
),
);
}
+
+ Widget _buildBottomTabIcon({
+ required bool active,
+ required String svgaPath,
+ required String fallbackPath,
+ }) {
+ return SCSvgaAssetWidget(
+ assetPath: svgaPath,
+ width: 35.w,
+ height: 35.w,
+ active: active,
+ loop: false,
+ fallback: Image.asset(fallbackPath, width: 35.w, height: 35.w),
+ );
+ }
}
diff --git a/lib/modules/room/seat/sc_seat_item.dart b/lib/modules/room/seat/sc_seat_item.dart
index ac61c19..1b15128 100644
--- a/lib/modules/room/seat/sc_seat_item.dart
+++ b/lib/modules/room/seat/sc_seat_item.dart
@@ -12,8 +12,8 @@ import 'package:yumi/shared/tools/sc_path_utils.dart';
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
-
-import '../../../shared/data_sources/models/enum/sc_room_special_mike_type.dart';
+import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
+import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
///麦位
class SCSeatItem extends StatefulWidget {
@@ -426,17 +426,44 @@ class _SonicState extends State with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
+ final isCurrentUserSeat = _isCurrentUserSeat();
return Stack(
clipBehavior: Clip.none,
children: [
- SonicItem(ctrList[0], widget.room, isGameModel: widget.isGameModel),
- SonicItem(ctrList[1], widget.room, isGameModel: widget.isGameModel),
- SonicItem(ctrList[2], widget.room, isGameModel: widget.isGameModel),
- SonicItem(ctrList[3], widget.room, isGameModel: widget.isGameModel),
+ SonicItem(
+ ctrList[0],
+ widget.room,
+ isGameModel: widget.isGameModel,
+ isCurrentUserSeat: isCurrentUserSeat,
+ ),
+ SonicItem(
+ ctrList[1],
+ widget.room,
+ isGameModel: widget.isGameModel,
+ isCurrentUserSeat: isCurrentUserSeat,
+ ),
+ SonicItem(
+ ctrList[2],
+ widget.room,
+ isGameModel: widget.isGameModel,
+ isCurrentUserSeat: isCurrentUserSeat,
+ ),
+ SonicItem(
+ ctrList[3],
+ widget.room,
+ isGameModel: widget.isGameModel,
+ isCurrentUserSeat: isCurrentUserSeat,
+ ),
],
);
}
+ bool _isCurrentUserSeat() {
+ final userId = provider.roomWheatMap[widget.index]?.user?.id;
+ return (userId?.isNotEmpty ?? false) &&
+ userId == AccountStorage().getCurrentUser()?.userProfile?.id;
+ }
+
void _checkSoundAni(int volume) async {
if (volume <= 20) return;
// await Future.delayed(Duration(milliseconds: 10));
@@ -472,67 +499,82 @@ class _SonicState extends State with TickerProviderStateMixin {
}
class SonicItem extends AnimatedWidget {
- Animation animation;
- JoinRoomRes? room;
- bool isGameModel = false;
+ final Animation animation;
+ final JoinRoomRes? room;
+ final bool isGameModel;
+ final bool isCurrentUserSeat;
- SonicItem(this.animation, this.room, {Key? key, this.isGameModel = false})
- : super(listenable: animation);
+ SonicItem(
+ this.animation,
+ this.room, {
+ super.key,
+ this.isGameModel = false,
+ required this.isCurrentUserSeat,
+ }) : super(listenable: animation);
@override
Widget build(BuildContext context) {
+ final specialMikeType = room?.roomProfile?.roomSetting?.roomSpecialMikeType;
+ final Widget fallbackWidget = _buildFallbackWidget(
+ specialMikeType,
+ animation.value,
+ );
+
return Visibility(
- visible: animation!.value > 0,
+ visible: animation.value > 0,
child: ScaleTransition(
- scale: Tween(begin: 1.0, end: 1.4).animate(animation!),
- child: Container(
+ scale: Tween(begin: 1.0, end: 1.4).animate(animation),
+ child: SizedBox(
height: width(isGameModel ? 34 : 52),
width: width(isGameModel ? 34 : 52),
- decoration:
- (room?.roomProfile?.roomSetting?.roomSpecialMikeType ?? "")
- .isEmpty ||
- (room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
- SCRoomSpecialMikeType.TYPE_VIP3.name &&
- room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
- SCRoomSpecialMikeType.TYPE_VIP4.name &&
- room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
- SCRoomSpecialMikeType.TYPE_VIP5.name &&
- room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
- SCRoomSpecialMikeType.TYPE_VIP6.name)
- ? BoxDecoration(
- shape: BoxShape.circle,
- color: SocialChatTheme.primaryColor.withOpacity(
- 1 - animation!.value,
- ),
- )
- : BoxDecoration(),
- child:
- room?.roomProfile?.roomSetting?.roomSpecialMikeType ==
- SCRoomSpecialMikeType.TYPE_VIP3.name
- ? Image.asset(
- "sc_images/room/sc_icon_room_vip3_sonic_anim.webp",
- )
- : (room?.roomProfile?.roomSetting?.roomSpecialMikeType ==
- SCRoomSpecialMikeType.TYPE_VIP4.name
- ? Image.asset(
- "sc_images/room/sc_icon_room_vip4_sonic_anim.webp",
- )
- : (room?.roomProfile?.roomSetting?.roomSpecialMikeType ==
- SCRoomSpecialMikeType.TYPE_VIP5.name
- ? Image.asset(
- "sc_images/room/sc_icon_room_vip5_sonic_anim.webp",
- )
- : (room
- ?.roomProfile
- ?.roomSetting
- ?.roomSpecialMikeType ==
- SCRoomSpecialMikeType.TYPE_VIP6.name
- ? Image.asset(
- "sc_images/room/sc_icon_room_vip6_sonic_anim.webp",
- )
- : Container()))),
+ child: SCSvgaAssetWidget(
+ assetPath: _resolveSvgaAssetPath(specialMikeType),
+ active: true,
+ loop: true,
+ fit: BoxFit.contain,
+ fallback: fallbackWidget,
+ ),
),
),
);
}
+
+ String _resolveSvgaAssetPath(String? specialMikeType) {
+ switch (specialMikeType) {
+ case "TYPE_VIP3":
+ return "sc_images/room/sc_icon_room_vip3_sonic_anim.svga";
+ case "TYPE_VIP4":
+ return "sc_images/room/sc_icon_room_vip4_sonic_anim.svga";
+ case "TYPE_VIP5":
+ return "sc_images/room/sc_icon_room_vip5_sonic_anim.svga";
+ case "TYPE_VIP6":
+ return "sc_images/room/sc_icon_room_vip6_sonic_anim.svga";
+ default:
+ return isCurrentUserSeat
+ ? "sc_images/room/sc_icon_room_self_sonic_anim.svga"
+ : "sc_images/room/sc_icon_room_other_sonic_anim.svga";
+ }
+ }
+
+ Widget _buildFallbackWidget(String? specialMikeType, double animationValue) {
+ switch (specialMikeType) {
+ case "TYPE_VIP3":
+ return Image.asset("sc_images/room/sc_icon_room_vip3_sonic_anim.webp");
+ case "TYPE_VIP4":
+ return Image.asset("sc_images/room/sc_icon_room_vip4_sonic_anim.webp");
+ case "TYPE_VIP5":
+ return Image.asset("sc_images/room/sc_icon_room_vip5_sonic_anim.webp");
+ case "TYPE_VIP6":
+ return Image.asset("sc_images/room/sc_icon_room_vip6_sonic_anim.webp");
+ default:
+ return DecoratedBox(
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: SocialChatTheme.primaryColor.withValues(
+ alpha: 1 - animationValue,
+ ),
+ ),
+ );
+ }
+ }
}
diff --git a/lib/modules/room_game/bridge/baishun_js_bridge.dart b/lib/modules/room_game/bridge/baishun_js_bridge.dart
index 973df5b..15a860b 100644
--- a/lib/modules/room_game/bridge/baishun_js_bridge.dart
+++ b/lib/modules/room_game/bridge/baishun_js_bridge.dart
@@ -7,6 +7,7 @@ class BaishunBridgeActions {
static const String gameLoaded = 'gameLoaded';
static const String gameRecharge = 'gameRecharge';
static const String destroy = 'destroy';
+ static const String debugLog = 'debugLog';
}
class BaishunBridgeMessage {
@@ -18,22 +19,50 @@ class BaishunBridgeMessage {
final String action;
final Map payload;
+ static BaishunBridgeMessage fromAction(String action, String rawMessage) {
+ return BaishunBridgeMessage(
+ action: action,
+ payload: _parsePayload(rawMessage),
+ );
+ }
+
static BaishunBridgeMessage parse(String rawMessage) {
try {
final dynamic decoded = jsonDecode(rawMessage);
if (decoded is Map) {
- final rawPayload = decoded['payload'];
return BaishunBridgeMessage(
- action: (decoded['action'] ?? '').toString(),
- payload:
- rawPayload is Map
- ? rawPayload
- : const {},
+ action:
+ (decoded['action'] ?? decoded['cmd'] ?? decoded['event'] ?? '')
+ .toString()
+ .trim(),
+ payload: _parsePayload(
+ decoded['payload'] ?? decoded['data'] ?? decoded['params'],
+ ),
);
}
} catch (_) {}
return BaishunBridgeMessage(action: rawMessage.trim());
}
+
+ static Map _parsePayload(dynamic rawPayload) {
+ if (rawPayload is Map) {
+ return rawPayload;
+ }
+ if (rawPayload is String) {
+ final trimmed = rawPayload.trim();
+ if (trimmed.isEmpty) {
+ return const {};
+ }
+ try {
+ final dynamic decoded = jsonDecode(trimmed);
+ if (decoded is Map) {
+ return decoded;
+ }
+ } catch (_) {}
+ return {'raw': trimmed};
+ }
+ return const {};
+ }
}
class BaishunJsBridge {
@@ -47,22 +76,251 @@ class BaishunJsBridge {
}
window.__baishunBridgeReady = true;
window.NativeBridge = window.NativeBridge || {};
- window.NativeBridge.getConfig = function(callback) {
- if (typeof callback === 'function') {
- window.__baishunGetConfigCallback = callback;
+ function toPayload(input) {
+ if (typeof input === 'function') {
+ window.__baishunGetConfigCallback = input;
+ return {};
}
- $channelName.postMessage(JSON.stringify({ action: '${BaishunBridgeActions.getConfig}' }));
+ 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 rememberJsCallback(payload) {
+ if (payload && typeof payload.jsCallback === 'string' && payload.jsCallback.trim()) {
+ window.__baishunLastJsCallback = payload.jsCallback.trim();
+ }
+ }
+ function postAction(action, input) {
+ const payload = toPayload(input);
+ rememberJsCallback(payload);
+ $channelName.postMessage(JSON.stringify({ action: action, payload: payload }));
+ return payload;
+ }
+ function clipText(value, limit) {
+ const text = (value == null ? '' : String(value)).trim();
+ if (!text) {
+ return '';
+ }
+ return text.length > limit ? text.slice(0, limit) + '...' : text;
+ }
+ function stringifyForLog(value) {
+ if (value == null) {
+ return '';
+ }
+ if (typeof value === 'string') {
+ return clipText(value, 320);
+ }
+ try {
+ return clipText(JSON.stringify(value), 320);
+ } catch (_) {
+ return clipText(String(value), 320);
+ }
+ }
+ function postDebug(tag, payload) {
+ $channelName.postMessage(JSON.stringify({
+ action: '${BaishunBridgeActions.debugLog}',
+ payload: {
+ tag: tag,
+ message: stringifyForLog(payload)
+ }
+ }));
+ }
+ function ensureChannelAlias(name, handler) {
+ if (!window[name] || typeof window[name].postMessage !== 'function') {
+ window[name] = { postMessage: handler };
+ }
+ }
+ function ensureWebkitHandler(name, handler) {
+ 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: handler };
+ }
+ }
+ window.NativeBridge.getConfigSync = function() {
+ return window.__baishunLastConfig || null;
+ };
+ window.NativeBridge.getConfig = function(params) {
+ postAction('${BaishunBridgeActions.getConfig}', params);
return window.__baishunLastConfig || null;
};
window.NativeBridge.destroy = function(payload) {
- $channelName.postMessage(JSON.stringify({ action: '${BaishunBridgeActions.destroy}', payload: payload || {} }));
+ postAction('${BaishunBridgeActions.destroy}', payload);
};
window.NativeBridge.gameRecharge = function(payload) {
- $channelName.postMessage(JSON.stringify({ action: '${BaishunBridgeActions.gameRecharge}', payload: payload || {} }));
+ postAction('${BaishunBridgeActions.gameRecharge}', payload);
};
window.NativeBridge.gameLoaded = function(payload) {
- $channelName.postMessage(JSON.stringify({ action: '${BaishunBridgeActions.gameLoaded}', payload: payload || {} }));
+ postAction('${BaishunBridgeActions.gameLoaded}', payload);
};
+ window.__baishunDebugLog = postDebug;
+ ensureChannelAlias('getConfig', function(message) {
+ window.NativeBridge.getConfig(message);
+ });
+ ensureChannelAlias('destroy', function(message) {
+ window.NativeBridge.destroy(message);
+ });
+ ensureChannelAlias('gameRecharge', function(message) {
+ window.NativeBridge.gameRecharge(message);
+ });
+ ensureChannelAlias('gameLoaded', function(message) {
+ window.NativeBridge.gameLoaded(message);
+ });
+ ensureWebkitHandler('getConfig', function(message) {
+ window.NativeBridge.getConfig(message);
+ });
+ ensureWebkitHandler('destroy', function(message) {
+ window.NativeBridge.destroy(message);
+ });
+ ensureWebkitHandler('gameRecharge', function(message) {
+ window.NativeBridge.gameRecharge(message);
+ });
+ ensureWebkitHandler('gameLoaded', function(message) {
+ window.NativeBridge.gameLoaded(message);
+ });
+ if (!window.__baishunNetworkDebugReady) {
+ window.__baishunNetworkDebugReady = true;
+ if (typeof window.addEventListener === 'function') {
+ window.addEventListener('error', function(event) {
+ const target = event && event.target;
+ if (target && target !== window) {
+ postDebug('resource.error', {
+ tag: target.tagName || '',
+ source: target.currentSrc || target.src || target.href || '',
+ outerHTML: target.outerHTML || ''
+ });
+ return;
+ }
+ postDebug('window.error', {
+ message: event && event.message,
+ source: event && event.filename,
+ line: event && event.lineno,
+ column: event && event.colno,
+ stack: event && event.error && event.error.stack
+ });
+ }, true);
+ window.addEventListener('unhandledrejection', function(event) {
+ postDebug('promise.reject', {
+ reason: event && event.reason
+ });
+ });
+ }
+ if (window.console && !window.__baishunConsoleDebugReady) {
+ window.__baishunConsoleDebugReady = true;
+ ['log', 'info', 'warn', 'error'].forEach(function(level) {
+ const original = typeof window.console[level] === 'function'
+ ? window.console[level].bind(window.console)
+ : null;
+ window.console[level] = function() {
+ try {
+ postDebug('console.' + level, Array.prototype.slice.call(arguments));
+ } catch (_) {}
+ if (original) {
+ return original.apply(window.console, arguments);
+ }
+ return undefined;
+ };
+ });
+ }
+ if (typeof window.fetch === 'function') {
+ const originalFetch = window.fetch.bind(window);
+ window.fetch = function(input, init) {
+ const url = typeof input === 'string' ? input : (input && input.url) || '';
+ const method = (init && init.method) || 'GET';
+ postDebug('fetch.start', { method: method, url: url });
+ return originalFetch(input, init)
+ .then(function(response) {
+ const cloned = response && typeof response.clone === 'function' ? response.clone() : null;
+ if (cloned && typeof cloned.text === 'function') {
+ cloned.text().then(function(text) {
+ if (!response.ok || /failed|error|exception|get user info/i.test(text)) {
+ postDebug('fetch.end', {
+ method: method,
+ url: url,
+ status: response.status,
+ body: text
+ });
+ } else {
+ postDebug('fetch.end', {
+ method: method,
+ url: url,
+ status: response.status
+ });
+ }
+ }).catch(function(error) {
+ postDebug('fetch.readError', {
+ method: method,
+ url: url,
+ error: error && error.message ? error.message : String(error)
+ });
+ });
+ } else {
+ postDebug('fetch.end', {
+ method: method,
+ url: url,
+ status: response && response.status
+ });
+ }
+ return response;
+ })
+ .catch(function(error) {
+ postDebug('fetch.error', {
+ method: method,
+ url: url,
+ error: error && error.message ? error.message : String(error)
+ });
+ throw error;
+ });
+ };
+ }
+ if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) {
+ const xhrOpen = window.XMLHttpRequest.prototype.open;
+ const xhrSend = window.XMLHttpRequest.prototype.send;
+ window.XMLHttpRequest.prototype.open = function(method, url) {
+ this.__baishunMethod = method || 'GET';
+ this.__baishunUrl = url || '';
+ return xhrOpen.apply(this, arguments);
+ };
+ window.XMLHttpRequest.prototype.send = function(body) {
+ const method = this.__baishunMethod || 'GET';
+ const url = this.__baishunUrl || '';
+ postDebug('xhr.start', { method: method, url: url });
+ this.addEventListener('loadend', function() {
+ const responseText = typeof this.responseText === 'string' ? this.responseText : '';
+ if (this.status >= 400 || /failed|error|exception|get user info/i.test(responseText)) {
+ postDebug('xhr.end', {
+ method: method,
+ url: url,
+ status: this.status,
+ body: responseText
+ });
+ } else {
+ postDebug('xhr.end', {
+ method: method,
+ url: url,
+ status: this.status
+ });
+ }
+ });
+ this.addEventListener('error', function() {
+ postDebug('xhr.error', {
+ method: method,
+ url: url,
+ status: this.status
+ });
+ });
+ return xhrSend.apply(this, arguments);
+ };
+ }
+ }
window.baishunChannel = window.baishunChannel || {};
window.baishunChannel.walletUpdate = function(payload) {
window.__baishunLastWalletPayload = payload || {};
@@ -72,35 +330,80 @@ class BaishunJsBridge {
if (typeof window.onWalletUpdate === 'function') {
window.onWalletUpdate(payload || {});
}
- if (typeof window.dispatchEvent === 'function') {
+ if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
window.dispatchEvent(new CustomEvent('walletUpdate', { detail: payload || {} }));
}
};
- if (typeof window.onBaishunBridgeReady === 'function') {
- window.onBaishunBridgeReady(window.NativeBridge);
- }
- if (typeof window.dispatchEvent === 'function') {
- window.dispatchEvent(new CustomEvent('baishunBridgeReady', { detail: { nativeBridge: true } }));
+ if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
+ window.dispatchEvent(new CustomEvent('baishunBridgeReady'));
}
})();
''';
}
- static String buildConfigScript(BaishunBridgeConfigModel config) {
+ static String buildConfigScript(
+ BaishunBridgeConfigModel config, {
+ String? jsCallbackPath,
+ }) {
final payload = jsonEncode(config.toJson());
+ final encodedCallbackPath = jsonEncode(jsCallbackPath?.trim() ?? '');
return '''
(function() {
const config = $payload;
+ const explicitCallbackPath = $encodedCallbackPath;
+ function resolvePath(path) {
+ if (!path || typeof path !== 'string') {
+ return null;
+ }
+ const parts = path.split('.').filter(Boolean);
+ if (!parts.length) {
+ return null;
+ }
+ let scope = window;
+ for (let i = 0; i < parts.length - 1; i += 1) {
+ scope = scope ? scope[parts[i]] : null;
+ }
+ if (!scope) {
+ return null;
+ }
+ const methodName = parts[parts.length - 1];
+ const method = scope[methodName];
+ if (typeof method !== 'function') {
+ return null;
+ }
+ return { scope: scope, method: method };
+ }
+ function invokeCallbackPath(path) {
+ const target = resolvePath(path);
+ if (!target) {
+ return false;
+ }
+ target.method.call(target.scope, config);
+ return true;
+ }
window.__baishunLastConfig = config;
+ window.baishunBridgeConfig = config;
+ window.NativeBridge = window.NativeBridge || {};
+ window.NativeBridge.config = config;
+ if (explicitCallbackPath) {
+ window.__baishunLastJsCallback = explicitCallbackPath;
+ }
if (typeof window.__baishunGetConfigCallback === 'function') {
window.__baishunGetConfigCallback(config);
+ window.__baishunGetConfigCallback = null;
+ }
+ if (window.__baishunLastJsCallback) {
+ invokeCallbackPath(window.__baishunLastJsCallback);
}
if (typeof window.onBaishunConfig === 'function') {
window.onBaishunConfig(config);
}
- if (typeof window.dispatchEvent === 'function') {
+ if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
window.dispatchEvent(new CustomEvent('baishunConfig', { detail: config }));
}
+ if (typeof document !== 'undefined' && typeof document.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
+ document.dispatchEvent(new CustomEvent('baishunConfig', { detail: config }));
+ }
})();
''';
}
@@ -119,26 +422,4 @@ class BaishunJsBridge {
})();
''';
}
-
- static String injectBootstrapHtml({
- required String html,
- required Uri entryUri,
- required BaishunBridgeConfigModel config,
- }) {
- final baseHref = htmlEscape.convert(entryUri.toString());
- final injection = '''
-
-
- ''';
-
- final headPattern = RegExp(r']*>', caseSensitive: false);
- final headMatch = headPattern.firstMatch(html);
- if (headMatch != null) {
- return html.replaceRange(headMatch.end, headMatch.end, injection);
- }
- return '$injection$html';
- }
}
diff --git a/lib/modules/room_game/views/baishun_debug_panel.dart b/lib/modules/room_game/views/baishun_debug_panel.dart
new file mode 100644
index 0000000..f6e0374
--- /dev/null
+++ b/lib/modules/room_game/views/baishun_debug_panel.dart
@@ -0,0 +1,445 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+class BaishunDebugLogEntry {
+ const BaishunDebugLogEntry({
+ required this.timestamp,
+ required this.tag,
+ required this.message,
+ });
+
+ final DateTime timestamp;
+ final String tag;
+ final String message;
+
+ String get formattedTime {
+ final hh = timestamp.hour.toString().padLeft(2, '0');
+ final mm = timestamp.minute.toString().padLeft(2, '0');
+ final ss = timestamp.second.toString().padLeft(2, '0');
+ return '$hh:$mm:$ss';
+ }
+}
+
+class BaishunDebugSnapshot {
+ const BaishunDebugSnapshot({
+ required this.roomId,
+ required this.gameName,
+ required this.gameId,
+ required this.gameSessionId,
+ required this.entryUrl,
+ required this.launchMode,
+ required this.loading,
+ required this.pageFinished,
+ required this.receivedBridgeMessage,
+ required this.injectCount,
+ required this.configSendCount,
+ required this.lastBridgeSource,
+ required this.lastBridgeAction,
+ required this.lastBridgePayload,
+ required this.lastJsCallback,
+ required this.lastConfigSummary,
+ required this.errorMessage,
+ required this.logs,
+ });
+
+ final String roomId;
+ final String gameName;
+ final String gameId;
+ final String gameSessionId;
+ final String entryUrl;
+ final String launchMode;
+ final bool loading;
+ final bool pageFinished;
+ final bool receivedBridgeMessage;
+ final int injectCount;
+ final int configSendCount;
+ final String lastBridgeSource;
+ final String lastBridgeAction;
+ final String lastBridgePayload;
+ final String lastJsCallback;
+ final String lastConfigSummary;
+ final String errorMessage;
+ final List logs;
+}
+
+class BaishunDebugPanel extends StatefulWidget {
+ const BaishunDebugPanel({
+ super.key,
+ required this.snapshot,
+ required this.onReload,
+ required this.onInjectBridge,
+ required this.onReplayConfig,
+ required this.onWalletUpdate,
+ required this.onClearLogs,
+ });
+
+ final BaishunDebugSnapshot snapshot;
+ final VoidCallback onReload;
+ final VoidCallback onInjectBridge;
+ final VoidCallback onReplayConfig;
+ final VoidCallback onWalletUpdate;
+ final VoidCallback onClearLogs;
+
+ @override
+ State createState() => _BaishunDebugPanelState();
+}
+
+class _BaishunDebugPanelState extends State {
+ bool _expanded = false;
+
+ @override
+ Widget build(BuildContext context) {
+ final maxWidth = (ScreenUtil().screenWidth - 24.w).clamp(220.w, 360.w);
+ return AnimatedSwitcher(
+ duration: const Duration(milliseconds: 180),
+ child:
+ _expanded
+ ? Container(
+ key: const ValueKey('baishun_debug_panel_expanded'),
+ width: maxWidth,
+ padding: EdgeInsets.all(12.w),
+ decoration: BoxDecoration(
+ color: const Color(0xE616302B),
+ borderRadius: BorderRadius.circular(16.w),
+ border: Border.all(
+ color: Colors.white.withValues(alpha: 0.14),
+ width: 1.w,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.22),
+ blurRadius: 18.w,
+ offset: Offset(0, 8.w),
+ ),
+ ],
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ 'BAISHUN DEBUG',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12.sp,
+ fontWeight: FontWeight.w800,
+ letterSpacing: 0.4,
+ ),
+ ),
+ const Spacer(),
+ GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () {
+ setState(() {
+ _expanded = false;
+ });
+ },
+ child: Padding(
+ padding: EdgeInsets.all(2.w),
+ child: Icon(
+ Icons.close,
+ size: 16.w,
+ color: Colors.white70,
+ ),
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 8.w),
+ _InfoLine(label: 'Room', value: widget.snapshot.roomId),
+ _InfoLine(
+ label: 'Game',
+ value:
+ '${widget.snapshot.gameName} (${widget.snapshot.gameId})',
+ ),
+ _InfoLine(
+ label: 'Session',
+ value: widget.snapshot.gameSessionId,
+ ),
+ _InfoLine(
+ label: 'Launch',
+ value: widget.snapshot.launchMode,
+ ),
+ _InfoLine(
+ label: 'Loading',
+ value: widget.snapshot.loading ? 'YES' : 'NO',
+ ),
+ _InfoLine(
+ label: 'PageFinished',
+ value: widget.snapshot.pageFinished ? 'YES' : 'NO',
+ ),
+ _InfoLine(
+ label: 'BridgeMsg',
+ value:
+ widget.snapshot.receivedBridgeMessage ? 'YES' : 'NO',
+ ),
+ _InfoLine(
+ label: 'InjectCount',
+ value: '${widget.snapshot.injectCount}',
+ ),
+ _InfoLine(
+ label: 'ConfigSend',
+ value: '${widget.snapshot.configSendCount}',
+ ),
+ _InfoLine(
+ label: 'LastSource',
+ value:
+ widget.snapshot.lastBridgeSource.isEmpty
+ ? '--'
+ : widget.snapshot.lastBridgeSource,
+ ),
+ _InfoLine(
+ label: 'LastAction',
+ value:
+ widget.snapshot.lastBridgeAction.isEmpty
+ ? '--'
+ : widget.snapshot.lastBridgeAction,
+ ),
+ _InfoLine(
+ label: 'LastCallback',
+ value:
+ widget.snapshot.lastJsCallback.isEmpty
+ ? '--'
+ : widget.snapshot.lastJsCallback,
+ ),
+ if (widget.snapshot.lastConfigSummary.isNotEmpty)
+ _MultilineLine(
+ label: 'Config',
+ value: widget.snapshot.lastConfigSummary,
+ ),
+ _MultilineLine(
+ label: 'EntryUrl',
+ value: widget.snapshot.entryUrl,
+ ),
+ if (widget.snapshot.lastBridgePayload.isNotEmpty)
+ _MultilineLine(
+ label: 'Payload',
+ value: widget.snapshot.lastBridgePayload,
+ ),
+ if (widget.snapshot.errorMessage.isNotEmpty)
+ _MultilineLine(
+ label: 'Error',
+ value: widget.snapshot.errorMessage,
+ valueColor: const Color(0xFFFFB4B4),
+ ),
+ SizedBox(height: 10.w),
+ Wrap(
+ spacing: 8.w,
+ runSpacing: 8.w,
+ children: [
+ _DebugActionChip(
+ label: 'Reload',
+ onTap: widget.onReload,
+ ),
+ _DebugActionChip(
+ label: 'Inject',
+ onTap: widget.onInjectBridge,
+ ),
+ _DebugActionChip(
+ label: 'ReplayConfig',
+ onTap: widget.onReplayConfig,
+ ),
+ _DebugActionChip(
+ label: 'Wallet',
+ onTap: widget.onWalletUpdate,
+ ),
+ _DebugActionChip(
+ label: 'ClearLogs',
+ onTap: widget.onClearLogs,
+ ),
+ ],
+ ),
+ SizedBox(height: 10.w),
+ Text(
+ 'Recent Logs',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 11.sp,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ SizedBox(height: 6.w),
+ Container(
+ height: 180.w,
+ padding: EdgeInsets.all(8.w),
+ decoration: BoxDecoration(
+ color: Colors.black.withValues(alpha: 0.22),
+ borderRadius: BorderRadius.circular(12.w),
+ border: Border.all(
+ color: Colors.white.withValues(alpha: 0.08),
+ ),
+ ),
+ child:
+ widget.snapshot.logs.isEmpty
+ ? Center(
+ child: Text(
+ 'No bridge logs yet',
+ style: TextStyle(
+ color: Colors.white54,
+ fontSize: 11.sp,
+ ),
+ ),
+ )
+ : ListView.separated(
+ itemCount: widget.snapshot.logs.length,
+ separatorBuilder:
+ (_, __) => SizedBox(height: 6.w),
+ itemBuilder: (context, index) {
+ final entry = widget.snapshot.logs[index];
+ return Text(
+ '[${entry.formattedTime}] ${entry.tag} ${entry.message}',
+ style: TextStyle(
+ color: Colors.white70,
+ fontSize: 10.sp,
+ height: 1.35,
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ )
+ : GestureDetector(
+ key: const ValueKey('baishun_debug_panel_collapsed'),
+ behavior: HitTestBehavior.opaque,
+ onTap: () {
+ setState(() {
+ _expanded = true;
+ });
+ },
+ child: Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: 12.w,
+ vertical: 8.w,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xD916302B),
+ borderRadius: BorderRadius.circular(999.w),
+ border: Border.all(
+ color: Colors.white.withValues(alpha: 0.12),
+ ),
+ ),
+ child: Text(
+ 'BS DEBUG',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 11.sp,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _InfoLine extends StatelessWidget {
+ const _InfoLine({required this.label, required this.value});
+
+ final String label;
+ final String value;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.only(bottom: 4.w),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: 82.w,
+ child: Text(
+ '$label:',
+ style: TextStyle(
+ color: Colors.white60,
+ fontSize: 10.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ Expanded(
+ child: Text(
+ value,
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 10.sp,
+ height: 1.3,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _MultilineLine extends StatelessWidget {
+ const _MultilineLine({
+ required this.label,
+ required this.value,
+ this.valueColor = Colors.white,
+ });
+
+ final String label;
+ final String value;
+ final Color valueColor;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.only(top: 4.w),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '$label:',
+ style: TextStyle(
+ color: Colors.white60,
+ fontSize: 10.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ SizedBox(height: 2.w),
+ SelectableText(
+ value,
+ style: TextStyle(color: valueColor, fontSize: 10.sp, height: 1.35),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _DebugActionChip extends StatelessWidget {
+ const _DebugActionChip({required this.label, required this.onTap});
+
+ final String label;
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: onTap,
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.w),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.08),
+ borderRadius: BorderRadius.circular(999.w),
+ border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
+ ),
+ child: Text(
+ label,
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 10.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/modules/room_game/views/baishun_game_page.dart b/lib/modules/room_game/views/baishun_game_page.dart
index 0bb982e..1651e70 100644
--- a/lib/modules/room_game/views/baishun_game_page.dart
+++ b/lib/modules/room_game/views/baishun_game_page.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -10,6 +9,7 @@ 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_debug_panel.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';
@@ -33,10 +33,23 @@ class BaishunGamePage extends StatefulWidget {
class _BaishunGamePageState extends State {
final RoomGameRepository _repository = RoomGameRepository();
late final WebViewController _controller;
+ Timer? _bridgeBootstrapTimer;
+ Timer? _loadingFallbackTimer;
+ final List _debugLogs = [];
bool _isLoading = true;
bool _isClosing = false;
+ bool _didReceiveBridgeMessage = false;
+ bool _didFinishPageLoad = false;
+ bool _hasDeliveredLaunchConfig = false;
+ int _bridgeInjectCount = 0;
+ int _configSendCount = 0;
String? _errorMessage;
+ String _lastBridgeSource = '';
+ String _lastBridgeAction = '';
+ String _lastBridgePayload = '';
+ String _lastJsCallback = '';
+ String _lastConfigSummary = '';
@override
void initState() {
@@ -49,12 +62,55 @@ class _BaishunGamePageState extends State {
BaishunJsBridge.channelName,
onMessageReceived: _handleBridgeMessage,
)
+ ..addJavaScriptChannel(
+ BaishunBridgeActions.getConfig,
+ onMessageReceived:
+ (JavaScriptMessage message) => _handleNamedChannelMessage(
+ BaishunBridgeActions.getConfig,
+ message,
+ ),
+ )
+ ..addJavaScriptChannel(
+ BaishunBridgeActions.destroy,
+ onMessageReceived:
+ (JavaScriptMessage message) => _handleNamedChannelMessage(
+ BaishunBridgeActions.destroy,
+ message,
+ ),
+ )
+ ..addJavaScriptChannel(
+ BaishunBridgeActions.gameRecharge,
+ onMessageReceived:
+ (JavaScriptMessage message) => _handleNamedChannelMessage(
+ BaishunBridgeActions.gameRecharge,
+ message,
+ ),
+ )
+ ..addJavaScriptChannel(
+ BaishunBridgeActions.gameLoaded,
+ onMessageReceived:
+ (JavaScriptMessage message) => _handleNamedChannelMessage(
+ BaishunBridgeActions.gameLoaded,
+ message,
+ ),
+ )
..setNavigationDelegate(
NavigationDelegate(
+ onPageStarted: (String _) {
+ _prepareForPageLoad();
+ _appendDebugLog('page', 'page started');
+ },
onPageFinished: (String _) async {
- await _primeBridgeHandshake();
+ _didFinishPageLoad = true;
+ _appendDebugLog('page', 'page finished');
+ await _injectBridge(reason: 'page_finished');
},
onWebResourceError: (WebResourceError error) {
+ _stopBridgeBootstrap();
+ _appendDebugLog(
+ 'error',
+ 'web resource error: ${error.description}',
+ );
if (!mounted) {
return;
}
@@ -68,9 +124,69 @@ class _BaishunGamePageState extends State {
unawaited(_loadGameEntry());
}
+ @override
+ void dispose() {
+ _stopBridgeBootstrap();
+ super.dispose();
+ }
+
+ void _prepareForPageLoad() {
+ _didReceiveBridgeMessage = false;
+ _didFinishPageLoad = false;
+ _hasDeliveredLaunchConfig = false;
+ _bridgeInjectCount = 0;
+ _configSendCount = 0;
+ _stopBridgeBootstrap();
+ _lastBridgeSource = '';
+ _lastBridgeAction = '';
+ _lastBridgePayload = '';
+ _lastJsCallback = '';
+ _lastConfigSummary = '';
+ _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;
+ }
+ _appendDebugLog('loading', 'timeout fallback, hide loading');
+ setState(() {
+ _isLoading = false;
+ });
+ });
+ if (!mounted) {
+ return;
+ }
+ setState(() {
+ _isLoading = true;
+ _errorMessage = null;
+ });
+ }
+
+ void _stopBridgeBootstrap() {
+ _bridgeBootstrapTimer?.cancel();
+ _bridgeBootstrapTimer = null;
+ _loadingFallbackTimer?.cancel();
+ _loadingFallbackTimer = null;
+ }
+
Future _loadGameEntry() async {
try {
+ _prepareForPageLoad();
final entryUrl = widget.launchModel.entry.entryUrl.trim();
+ _appendDebugLog(
+ 'launch',
+ 'load entry launchMode=${widget.launchModel.entry.launchMode} url=$entryUrl',
+ );
if (entryUrl.isEmpty || entryUrl.startsWith('mock://')) {
final html = await rootBundle.loadString(
'assets/debug/baishun_mock/index.html',
@@ -86,14 +202,9 @@ class _BaishunGamePageState extends State {
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());
+ await _controller.loadRequest(uri);
} catch (error) {
+ _appendDebugLog('error', 'load entry failed: $error');
if (!mounted) {
return;
}
@@ -104,57 +215,131 @@ class _BaishunGamePageState extends State {
}
}
- 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({String reason = 'manual'}) async {
+ _bridgeInjectCount += 1;
+ if (reason != 'bootstrap') {
+ _appendDebugLog('inject', 'reason=$reason count=$_bridgeInjectCount');
}
- }
-
- Future _injectBridge() async {
try {
await _controller.runJavaScript(BaishunJsBridge.bootstrapScript());
+ } catch (_) {}
+ }
+
+ Future _sendConfigToGame({
+ required String reason,
+ String? jsCallbackPath,
+ bool force = false,
+ }) async {
+ if (_hasDeliveredLaunchConfig && !force) {
+ _appendDebugLog(
+ 'config',
+ 'skip duplicate config send because BAISHUN code is one-time',
+ );
+ return;
+ }
+ _configSendCount += 1;
+ _hasDeliveredLaunchConfig = true;
+ final rawConfig = widget.launchModel.bridgeConfig;
+ final config = _buildEffectiveBridgeConfig(rawConfig);
+ final code = config.code.trim();
+ final maskedCode =
+ code.length <= 8
+ ? code
+ : '${code.substring(0, 4)}...${code.substring(code.length - 4)}';
+ _lastConfigSummary =
+ 'appChannel=${config.appChannel}, appId=${config.appId}, userId=${config.userId}, roomId=${config.roomId}, gameMode=${config.gameMode}, language=${config.language}(raw=${rawConfig.language}), gsp=${config.gsp}, code=$maskedCode(len=${code.length})';
+ if (config.language != rawConfig.language.trim()) {
+ _appendDebugLog(
+ 'config',
+ 'normalize language ${rawConfig.language} -> ${config.language} by BAISHUN table',
+ );
+ }
+ _appendDebugLog(
+ 'config',
+ 'reason=$reason count=$_configSendCount callback=${jsCallbackPath?.trim().isNotEmpty == true ? jsCallbackPath!.trim() : '--'} $_lastConfigSummary',
+ );
+ if (mounted) {
+ setState(() {});
+ }
+ try {
await _controller.runJavaScript(
- BaishunJsBridge.buildConfigScript(widget.launchModel.bridgeConfig),
+ BaishunJsBridge.buildConfigScript(
+ config,
+ jsCallbackPath: jsCallbackPath,
+ ),
);
} 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);
+ await _dispatchBridgeMessage(
+ bridgeMessage,
+ source: BaishunJsBridge.channelName,
+ rawMessage: message.message,
+ );
+ }
+
+ Future _handleNamedChannelMessage(
+ String action,
+ JavaScriptMessage message,
+ ) async {
+ final bridgeMessage = BaishunBridgeMessage.fromAction(
+ action,
+ message.message,
+ );
+ await _dispatchBridgeMessage(
+ bridgeMessage,
+ source: 'channel:$action',
+ rawMessage: message.message,
+ );
+ }
+
+ Future _dispatchBridgeMessage(
+ BaishunBridgeMessage bridgeMessage, {
+ required String source,
+ required String rawMessage,
+ }) async {
+ if (bridgeMessage.action == BaishunBridgeActions.debugLog) {
+ final tag = bridgeMessage.payload['tag']?.toString().trim();
+ final message = bridgeMessage.payload['message']?.toString().trim();
+ if ((tag ?? '').isNotEmpty || (message ?? '').isNotEmpty) {
+ _appendDebugLog(
+ tag == null || tag.isEmpty ? 'h5' : 'h5:$tag',
+ message == null || message.isEmpty ? rawMessage.trim() : message,
+ );
+ }
+ return;
+ }
+ final callbackPath = _extractCallbackPath(bridgeMessage.payload);
+ if (bridgeMessage.action.isNotEmpty) {
+ _didReceiveBridgeMessage = true;
+ _stopBridgeBootstrap();
+ _lastBridgeSource = source;
+ _lastBridgeAction = bridgeMessage.action;
+ _lastBridgePayload = _formatPayload(bridgeMessage.payload);
+ if (callbackPath.isNotEmpty) {
+ _lastJsCallback = callbackPath;
+ }
+ _appendDebugLog(
+ 'bridge',
+ '$source -> ${bridgeMessage.action}${callbackPath.isEmpty ? '' : ' callback=$callbackPath'}${_lastBridgePayload.isEmpty ? '' : ' payload=$_lastBridgePayload'}',
+ );
+ if (mounted) {
+ setState(() {});
+ }
+ } else if (rawMessage.trim().isNotEmpty) {
+ _appendDebugLog('bridge', '$source -> raw=${rawMessage.trim()}');
+ }
switch (bridgeMessage.action) {
case BaishunBridgeActions.getConfig:
- await _controller.runJavaScript(
- BaishunJsBridge.buildConfigScript(widget.launchModel.bridgeConfig),
+ await _sendConfigToGame(
+ reason: 'get_config',
+ jsCallbackPath: callbackPath.isEmpty ? null : callbackPath,
);
break;
case BaishunBridgeActions.gameLoaded:
+ _appendDebugLog('loading', 'gameLoaded received');
if (!mounted) {
return;
}
@@ -164,10 +349,12 @@ class _BaishunGamePageState extends State {
});
break;
case BaishunBridgeActions.gameRecharge:
+ _appendDebugLog('bridge', 'open recharge page');
await SCNavigatorUtils.push(context, WalletRoute.recharge);
await _notifyWalletUpdate();
break;
case BaishunBridgeActions.destroy:
+ _appendDebugLog('bridge', 'destroy requested by H5');
await _closeAndExit(reason: 'h5_destroy');
break;
default:
@@ -176,6 +363,7 @@ class _BaishunGamePageState extends State {
}
Future _notifyWalletUpdate() async {
+ _appendDebugLog('wallet', 'send walletUpdate to H5');
try {
await _controller.runJavaScript(
BaishunJsBridge.buildWalletUpdateScript(),
@@ -187,13 +375,39 @@ class _BaishunGamePageState extends State {
if (!mounted) {
return;
}
- setState(() {
- _errorMessage = null;
- _isLoading = true;
- });
+ _appendDebugLog('panel', 'manual reload');
await _loadGameEntry();
}
+ Future _manualInjectBridge() async {
+ await _injectBridge(reason: 'manual');
+ }
+
+ Future _replayLastConfig() async {
+ final callbackPath = _lastJsCallback.trim();
+ _appendDebugLog(
+ 'panel',
+ callbackPath.isEmpty
+ ? 'manual replay config without jsCallback'
+ : 'manual replay config to $callbackPath',
+ );
+ await _sendConfigToGame(
+ reason: 'manual_replay',
+ jsCallbackPath: callbackPath.isEmpty ? null : callbackPath,
+ force: true,
+ );
+ }
+
+ void _clearDebugLogs() {
+ if (!mounted) {
+ return;
+ }
+ setState(() {
+ _debugLogs.clear();
+ });
+ _appendDebugLog('panel', 'logs cleared');
+ }
+
Future _closeAndExit({String reason = 'page_back'}) async {
if (_isClosing) {
return;
@@ -213,6 +427,138 @@ class _BaishunGamePageState extends State {
}
}
+ String _extractCallbackPath(Map payload) {
+ final candidates = [
+ payload['jsCallback']?.toString() ?? '',
+ payload['callback']?.toString() ?? '',
+ ];
+ for (final candidate in candidates) {
+ final trimmed = candidate.trim();
+ if (trimmed.isNotEmpty) {
+ return trimmed;
+ }
+ }
+ return '';
+ }
+
+ BaishunBridgeConfigModel _buildEffectiveBridgeConfig(
+ BaishunBridgeConfigModel rawConfig,
+ ) {
+ final normalizedLanguage = _mapBaishunLanguage(rawConfig.language);
+ if (normalizedLanguage == rawConfig.language.trim()) {
+ return rawConfig;
+ }
+ return BaishunBridgeConfigModel(
+ appName: rawConfig.appName,
+ appChannel: rawConfig.appChannel,
+ appId: rawConfig.appId,
+ userId: rawConfig.userId,
+ code: rawConfig.code,
+ roomId: rawConfig.roomId,
+ gameMode: rawConfig.gameMode,
+ language: normalizedLanguage,
+ gsp: rawConfig.gsp,
+ gameConfig: rawConfig.gameConfig,
+ );
+ }
+
+ String _mapBaishunLanguage(String rawLanguage) {
+ final trimmed = rawLanguage.trim();
+ if (trimmed.isEmpty) {
+ return '2';
+ }
+ final lower = trimmed.toLowerCase();
+ const supportedCodes = {
+ '0',
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ '10',
+ '11',
+ };
+ if (supportedCodes.contains(lower)) {
+ return lower;
+ }
+ if (lower.startsWith('zh')) {
+ return '0';
+ }
+ switch (lower) {
+ case 'en':
+ case 'english':
+ return '2';
+ case 'id':
+ case 'in':
+ case 'indonesian':
+ return '3';
+ case 'ms':
+ case 'malay':
+ return '4';
+ case 'th':
+ case 'thai':
+ return '5';
+ case 'vi':
+ case 'vietnamese':
+ return '6';
+ case 'ar':
+ case 'arabic':
+ return '7';
+ case 'fil':
+ case 'tl':
+ case 'tagalog':
+ return '8';
+ case 'pt':
+ case 'portuguese':
+ return '9';
+ case 'tr':
+ case 'turkish':
+ return '10';
+ case 'ur':
+ case 'urdu':
+ return '11';
+ default:
+ return '2';
+ }
+ }
+
+ String _formatPayload(Map payload) {
+ if (payload.isEmpty) {
+ return '';
+ }
+ try {
+ return jsonEncode(payload);
+ } catch (_) {
+ return payload.toString();
+ }
+ }
+
+ void _appendDebugLog(String tag, String message) {
+ const maxLogEntries = 80;
+ final entry = BaishunDebugLogEntry(
+ timestamp: DateTime.now(),
+ tag: tag,
+ message: message,
+ );
+ if (!mounted) {
+ _debugLogs.insert(0, entry);
+ if (_debugLogs.length > maxLogEntries) {
+ _debugLogs.removeRange(maxLogEntries, _debugLogs.length);
+ }
+ return;
+ }
+ setState(() {
+ _debugLogs.insert(0, entry);
+ if (_debugLogs.length > maxLogEntries) {
+ _debugLogs.removeRange(maxLogEntries, _debugLogs.length);
+ }
+ });
+ }
+
@override
Widget build(BuildContext context) {
return PopScope(
@@ -281,12 +627,42 @@ class _BaishunGamePageState extends State {
),
if (_errorMessage != null) _buildErrorState(),
if (_isLoading && _errorMessage == null)
- const IgnorePointer(
- ignoring: true,
- child: BaishunLoadingView(
- message: 'Waiting for gameLoaded...',
- ),
+ const BaishunLoadingView(
+ message: 'Waiting for gameLoaded...',
),
+ Positioned(
+ left: 12.w,
+ bottom: 12.w,
+ child: BaishunDebugPanel(
+ snapshot: BaishunDebugSnapshot(
+ roomId: widget.roomId,
+ gameName: widget.game.name,
+ gameId: widget.game.gameId,
+ gameSessionId: widget.launchModel.gameSessionId,
+ entryUrl: widget.launchModel.entry.entryUrl,
+ launchMode: widget.launchModel.entry.launchMode,
+ loading: _isLoading,
+ pageFinished: _didFinishPageLoad,
+ receivedBridgeMessage: _didReceiveBridgeMessage,
+ injectCount: _bridgeInjectCount,
+ configSendCount: _configSendCount,
+ lastBridgeSource: _lastBridgeSource,
+ lastBridgeAction: _lastBridgeAction,
+ lastBridgePayload: _lastBridgePayload,
+ lastJsCallback: _lastJsCallback,
+ lastConfigSummary: _lastConfigSummary,
+ errorMessage: _errorMessage ?? '',
+ logs: List.unmodifiable(
+ _debugLogs,
+ ),
+ ),
+ onReload: _reload,
+ onInjectBridge: _manualInjectBridge,
+ onReplayConfig: _replayLastConfig,
+ onWalletUpdate: _notifyWalletUpdate,
+ onClearLogs: _clearDebugLogs,
+ ),
+ ),
],
),
),
diff --git a/lib/modules/user/edit/edit_user_info_page2.dart b/lib/modules/user/edit/edit_user_info_page2.dart
index 50c36fc..f5814fb 100644
--- a/lib/modules/user/edit/edit_user_info_page2.dart
+++ b/lib/modules/user/edit/edit_user_info_page2.dart
@@ -8,7 +8,6 @@ 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/repositories/sc_user_repository_impl.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
-import 'package:yumi/main.dart';
import 'package:provider/provider.dart';
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
@@ -16,23 +15,23 @@ import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
-import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.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/usecases/sc_accurate_length_limiting_textInput_formatter.dart';
-import '../../country/country_route.dart';
class EditUserInfoPage2 extends StatefulWidget {
- const EditUserInfoPage2({Key? key}) : super(key: key);
+ const EditUserInfoPage2({super.key});
@override
- _EditUserInfoPage2State createState() => _EditUserInfoPage2State();
+ State createState() => _EditUserInfoPage2State();
}
class _EditUserInfoPage2State extends State
with SingleTickerProviderStateMixin {
+ static const Object _noChange = Object();
+
String? userCover;
String? bornMonth;
@@ -44,45 +43,22 @@ class _EditUserInfoPage2State extends State
DateTime? birthdayDate;
num? sex;
num? age;
- Country? country;
+ bool _isSubmitting = false;
@override
void initState() {
super.initState();
- userCover =
- AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "";
- nickName =
- AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? "";
- autograph = AccountStorage().getCurrentUser()?.userProfile?.autograph ?? "";
- hobby = AccountStorage().getCurrentUser()?.userProfile?.hobby ?? "";
- country = Provider.of(
- context,
- listen: false,
- ).findCountryByName(
- AccountStorage().getCurrentUser()?.userProfile?.countryName ?? "",
- );
- num m = AccountStorage().getCurrentUser()?.userProfile?.bornMonth ?? 0;
- if (m < 10) {
- bornMonth = "0$m";
- } else {
- bornMonth = "$m";
- }
- num d = AccountStorage().getCurrentUser()?.userProfile?.bornDay ?? 0;
- if (d < 10) {
- bornDay = "0$d";
- } else {
- bornDay = "$d";
- }
- bornYear = "${AccountStorage().getCurrentUser()?.userProfile?.bornYear}";
- age =
- DateTime.now().year -
- (AccountStorage().getCurrentUser()?.userProfile?.bornYear ?? 0);
- sex = AccountStorage().getCurrentUser()?.userProfile?.userSex;
- }
-
- @override
- void dispose() {
- super.dispose();
+ final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
+ userCover = currentProfile?.userAvatar ?? "";
+ nickName = currentProfile?.userNickname ?? "";
+ autograph = currentProfile?.autograph ?? "";
+ hobby = currentProfile?.hobby ?? "";
+ birthdayDate = _birthdayFromProfile(currentProfile);
+ bornMonth = _formatTwoDigits(currentProfile?.bornMonth);
+ bornDay = _formatTwoDigits(currentProfile?.bornDay);
+ bornYear = _formatYear(currentProfile?.bornYear);
+ age = currentProfile?.age ?? _ageFromBirthday(birthdayDate);
+ sex = currentProfile?.userSex;
}
String? _preferNonEmpty(String? primary, String? fallback) {
@@ -110,6 +86,77 @@ class _EditUserInfoPage2State extends State
return _preferNonEmpty(primary, fallback);
}
+ DateTime? _birthdayFromProfile([SocialChatUserProfile? profile]) {
+ final targetProfile =
+ profile ?? AccountStorage().getCurrentUser()?.userProfile;
+ final year = targetProfile?.bornYear?.toInt() ?? 0;
+ final month = targetProfile?.bornMonth?.toInt() ?? 0;
+ final day = targetProfile?.bornDay?.toInt() ?? 0;
+ if (year <= 0 || month <= 0 || day <= 0) {
+ return null;
+ }
+ try {
+ return DateTime(year, month, day);
+ } catch (_) {
+ return null;
+ }
+ }
+
+ String _formatTwoDigits(num? value) {
+ final intValue = value?.toInt() ?? 0;
+ if (intValue <= 0) {
+ return "";
+ }
+ return intValue < 10 ? "0$intValue" : "$intValue";
+ }
+
+ String _formatYear(num? value) {
+ final intValue = value?.toInt() ?? 0;
+ if (intValue <= 0) {
+ return "";
+ }
+ return "$intValue";
+ }
+
+ num? _ageFromBirthday(DateTime? date) {
+ if (date == null) {
+ return null;
+ }
+ return (DateTime.now().year - date.year).abs();
+ }
+
+ String _birthdayDisplayText() {
+ if ((bornYear ?? "").isEmpty ||
+ (bornMonth ?? "").isEmpty ||
+ (bornDay ?? "").isEmpty) {
+ return "";
+ }
+ return "$bornYear-$bornMonth-$bornDay";
+ }
+
+ bool _isSameDate(DateTime first, DateTime second) {
+ return first.year == second.year &&
+ first.month == second.month &&
+ first.day == second.day;
+ }
+
+ bool _hasChanges(List