banner参数 非全面屏适配 语言房进入逻辑修改

This commit is contained in:
roxy 2026-04-27 13:50:43 +08:00
parent 39464469ba
commit 83fb6465f9
16 changed files with 750 additions and 111 deletions

View File

@ -651,6 +651,8 @@
"pleaseGetOnTheMicFirst": "يرجى الصعود إلى الميكروفون أولاً.",
"welcomeMessage": "مرحبًا بك في تطبيقنا، {name}!",
"operationFail": "فشلت العملية.",
"enterRoomFailedRetry": "فشل دخول الغرفة. يرجى المحاولة مرة أخرى.",
"voiceConnectionFailedRetry": "فشل الاتصال الصوتي. يرجى المحاولة مرة أخرى.",
"doYouWantToKeepTheDraft": "هل تريد الاحتفاظ بالمسودة؟",
"duration2": "المدة:{1}"
}

View File

@ -268,6 +268,8 @@
"dice": "ডাইস",
"rps": "পাথর-কাগজ-কাঁচি",
"operationFail": "অপারেশন ব্যর্থ হয়েছে।",
"enterRoomFailedRetry": "রুমে প্রবেশ ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
"voiceConnectionFailedRetry": "ভয়েস সংযোগ ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
"likedYourComment": "আপনার মন্তব্য পছন্দ করেছেন।",
"doYouWantToKeepTheDraft": "আপনি কি ড্রাফট রাখতে চান?",
"operationsAreTooFrequent": "অপারেশন খুব ঘন ঘন",

View File

@ -239,6 +239,8 @@
"dice": "Dice",
"rps": "RPS",
"operationFail": "The operation failed.",
"enterRoomFailedRetry": "Failed to enter the room. Please try again.",
"voiceConnectionFailedRetry": "Voice connection failed. Please try again.",
"likedYourComment": "Liked your comment.",
"doYouWantToKeepTheDraft": "Do you want to keep the draft?",
"operationsAreTooFrequent": "Operations are too frequent",

View File

@ -237,8 +237,10 @@
"win": "Kazanan",
"dice": "Zar",
"rps": "Taş-Kağıt-Makas",
"operationFail": "İşlem başarısız oldu.",
"likedYourComment": "Yorumunuzu beğendi.",
"operationFail": "İşlem başarısız oldu.",
"enterRoomFailedRetry": "Odaya girilemedi. Lütfen tekrar deneyin.",
"voiceConnectionFailedRetry": "Ses bağlantısı başarısız oldu. Lütfen tekrar deneyin.",
"likedYourComment": "Yorumunuzu beğendi.",
"doYouWantToKeepTheDraft": "Taslağı saklamak ister misiniz?",
"operationsAreTooFrequent": "İşlemler çok sık",
"luckNumber": "Şans Numarası",

View File

@ -239,6 +239,20 @@ class SCAppLocalizations {
String get operationFail => translate('operationFail');
String get enterRoomFailedRetry {
final value = translate('enterRoomFailedRetry');
return value == 'enterRoomFailedRetry'
? 'Failed to enter the room. Please try again.'
: value;
}
String get voiceConnectionFailedRetry {
final value = translate('voiceConnectionFailedRetry');
return value == 'voiceConnectionFailedRetry'
? 'Voice connection failed. Please try again.'
: value;
}
String get forMoreRewardsPleaseCheckTheTaskCenter =>
translate('forMoreRewardsPleaseCheckTheTaskCenter');

View File

@ -1,7 +1,10 @@
import 'dart:math' as math;
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
@ -60,6 +63,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
RoomGiftSeatFlightController();
final Map<String, _LuckyGiftComboSession> _luckyGiftComboSessions =
<String, _LuckyGiftComboSession>{};
int _shownRoomStartupFailureToken = 0;
@override
void initState() {
@ -154,6 +158,89 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
SCGiftVapSvgaManager().stopPlayback();
}
Widget _buildRoomStartupLayer() {
return Selector<RtcProvider, _RoomStartupSnapshot>(
selector:
(context, provider) => _RoomStartupSnapshot(
status: provider.roomStartupStatus,
failureType: provider.roomStartupFailureType,
failureToken: provider.roomStartupFailureToken,
),
builder: (context, snapshot, child) {
if (snapshot.status == RoomStartupStatus.failed) {
_showRoomStartupFailureDialog(snapshot);
return const SizedBox.shrink();
}
if (snapshot.status != RoomStartupStatus.loading) {
return const SizedBox.shrink();
}
return Positioned.fill(
child: AbsorbPointer(
child: Container(
color: Colors.black.withValues(alpha: 0.18),
alignment: Alignment.center,
child: Container(
width: 55.w,
height: 55.w,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(8.w),
),
alignment: Alignment.center,
child: const CupertinoActivityIndicator(color: Colors.white24),
),
),
),
);
},
);
}
void _showRoomStartupFailureDialog(_RoomStartupSnapshot snapshot) {
if (_shownRoomStartupFailureToken == snapshot.failureToken) {
return;
}
_shownRoomStartupFailureToken = snapshot.failureToken;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
SmartDialog.dismiss(tag: "roomStartupFailureDialog");
SmartDialog.show(
tag: "roomStartupFailureDialog",
alignment: Alignment.center,
animationType: SmartAnimationType.fade,
backType: SmartBackType.block,
clickMaskDismiss: false,
builder: (_) {
return _RoomStartupFailureDialog(
title: SCAppLocalizations.of(context)!.tips,
message: _roomStartupFailureMessage(snapshot.failureType),
btnText: SCAppLocalizations.of(context)!.confirm,
onEnsure: () {
SmartDialog.dismiss(tag: "roomStartupFailureDialog");
context.read<RtcProvider>().confirmRoomStartupFailureAndLeave(
navigationContext: context,
);
},
);
},
);
});
}
String _roomStartupFailureMessage(RoomStartupFailureType failureType) {
switch (failureType) {
case RoomStartupFailureType.im:
return SCAppLocalizations.of(context)!.enterRoomFailedRetry;
case RoomStartupFailureType.rtc:
return SCAppLocalizations.of(context)!.voiceConnectionFailedRetry;
case RoomStartupFailureType.entry:
case RoomStartupFailureType.none:
return SCAppLocalizations.of(context)!.operationFail;
}
}
String? _resolveRoomThemeBackground(JoinRoomRes? room) {
final roomTheme = room?.roomProps?.roomTheme;
final themeBack = roomTheme?.themeBack ?? "";
@ -172,6 +259,11 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (!didPop) {
final rtcProvider = context.read<RtcProvider>();
if (rtcProvider.roomStartupStatus == RoomStartupStatus.loading ||
rtcProvider.roomStartupStatus == RoomStartupStatus.failed) {
return;
}
_suspendRoomVisualEffects();
SCFloatIchart().show();
SCNavigatorUtils.goBack(context);
@ -231,6 +323,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
),
],
),
_buildRoomStartupLayer(),
// _buildPlayViews(),
///
LuckGiftNomorAnimWidget(),
@ -727,3 +820,110 @@ class _LuckyGiftComboSession {
clearQueueTimer?.cancel();
}
}
class _RoomStartupSnapshot {
const _RoomStartupSnapshot({
required this.status,
required this.failureType,
required this.failureToken,
});
final RoomStartupStatus status;
final RoomStartupFailureType failureType;
final int failureToken;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is _RoomStartupSnapshot &&
other.status == status &&
other.failureType == failureType &&
other.failureToken == failureToken;
}
@override
int get hashCode => Object.hash(status, failureType, failureToken);
}
class _RoomStartupFailureDialog extends StatelessWidget {
const _RoomStartupFailureDialog({
required this.title,
required this.message,
required this.btnText,
required this.onEnsure,
});
final String title;
final String message;
final String btnText;
final VoidCallback onEnsure;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 320.w),
margin: EdgeInsets.symmetric(horizontal: 22.w),
padding: EdgeInsets.fromLTRB(18.w, 18.w, 18.w, 30.w),
decoration: BoxDecoration(
color: const Color(0xff09372E),
borderRadius: BorderRadius.circular(12.w),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w700,
decoration: TextDecoration.none,
),
),
SizedBox(height: 14.w),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
height: 1.25,
decoration: TextDecoration.none,
),
),
SizedBox(height: 18.w),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onEnsure,
child: Container(
width: 160.w,
height: 38.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xff18F2B1),
borderRadius: BorderRadius.circular(12.w),
),
child: Text(
btnText,
style: TextStyle(
color: Colors.black,
fontSize: 14.sp,
fontWeight: FontWeight.w700,
decoration: TextDecoration.none,
),
),
),
),
],
),
),
),
);
}
}

View File

@ -388,8 +388,11 @@ class BaishunJsBridge {
static String buildConfigScript(
BaishunBridgeConfigModel config, {
String? jsCallbackPath,
Map<String, dynamic> viewportPayload = const <String, dynamic>{},
}) {
final payload = jsonEncode(config.toJson());
final payload = jsonEncode(
_mergeViewportPayload(config.toJson(), viewportPayload),
);
final encodedCallbackPath = jsonEncode(jsCallbackPath?.trim() ?? '');
return '''
(function() {
@ -437,8 +440,15 @@ class BaishunJsBridge {
}
window.__baishunLastConfig = config;
window.baishunBridgeConfig = config;
const viewport = config.viewport || config.nativeViewport || {};
window.__baishunViewport = viewport;
window.baishunViewport = viewport;
window.NativeBridge = window.NativeBridge || {};
window.NativeBridge.config = config;
window.NativeBridge.viewport = viewport;
window.NativeBridge.getViewport = function() {
return viewport;
};
const callbackPath = explicitCallbackPath || window.__baishunLastJsCallback || '';
let callbackInvoked = false;
if (explicitCallbackPath) {
@ -455,6 +465,7 @@ class BaishunJsBridge {
gsp: config.gsp,
code: maskText(config.code),
codeLength: config.code ? String(config.code).length : 0,
viewport: config.viewport || config.nativeViewport || null,
callbackPath: callbackPath
});
}
@ -486,6 +497,30 @@ class BaishunJsBridge {
''';
}
static Map<String, dynamic> _mergeViewportPayload(
Map<String, dynamic> payload,
Map<String, dynamic> viewport,
) {
if (viewport.isEmpty) {
return payload;
}
return <String, dynamic>{
...payload,
'viewport': viewport,
'nativeViewport': viewport,
'containerWidth': viewport['containerWidth'],
'containerHeight': viewport['containerHeight'],
'visibleHeight': viewport['visibleHeight'],
'webViewHeight': viewport['webViewHeight'],
'dialogHeight': viewport['dialogHeight'],
'safeAreaTop': viewport['safeAreaTop'],
'safeAreaBottom': viewport['safeAreaBottom'],
'devicePixelRatio': viewport['devicePixelRatio'],
'screenMode': viewport['screenMode'],
'isCompactViewport': viewport['isCompactViewport'],
};
}
static String buildWalletUpdateScript({Map<String, dynamic>? payload}) {
final safePayload = jsonEncode(
payload ??

View File

@ -172,6 +172,14 @@ class LeaderJsBridge {
normalizedPayload.entry && typeof normalizedPayload.entry === 'object'
? normalizedPayload.entry
: {};
const viewport =
normalizedPayload.viewport && typeof normalizedPayload.viewport === 'object'
? normalizedPayload.viewport
: (
launchConfig.viewport && typeof launchConfig.viewport === 'object'
? launchConfig.viewport
: {}
);
window.__leaderLaunchPayload = normalizedPayload;
window.__leaderLaunchPayloadText = payloadText;
window.leaderLaunchPayload = normalizedPayload;
@ -179,14 +187,21 @@ class LeaderJsBridge {
window.leaderLaunchConfig = launchConfig;
window.__leaderLaunchEntry = entry;
window.leaderLaunchEntry = entry;
window.__leaderViewport = viewport;
window.leaderViewport = viewport;
window.NativeBridge = window.NativeBridge || {};
window.NativeBridge.config = launchConfig;
window.NativeBridge.launchConfig = launchConfig;
window.NativeBridge.entry = entry;
window.NativeBridge.viewport = viewport;
window.NativeBridge.getConfig = function(callback) {
invokeCallback(callback, launchConfig);
return launchConfig;
};
window.NativeBridge.getViewport = function(callback) {
invokeCallback(callback, viewport);
return viewport;
};
window.NativeBridge.getLaunchPayload = function(callback) {
invokeCallback(callback, normalizedPayload);
return normalizedPayload;
@ -207,7 +222,8 @@ class LeaderJsBridge {
? String(launchConfig.code).slice(0, 6) + '***'
: '',
codeLength: launchConfig.code ? String(launchConfig.code).length : 0,
entryUrl: entry.entryUrl || ''
entryUrl: entry.entryUrl || '',
viewport: viewport
});
}
}

View File

@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
const double roomGameDesignWidth = 750;
double resolveRoomGameAvailableHeight(
BuildContext context, {
double reservedTop = 12,
}) {
final mediaQuery = MediaQuery.maybeOf(context);
if (mediaQuery == null) {
return 0;
}
final bottomInset =
mediaQuery.viewInsets.bottom > mediaQuery.padding.bottom
? mediaQuery.viewInsets.bottom
: mediaQuery.padding.bottom;
final availableHeight =
mediaQuery.size.height -
mediaQuery.padding.top -
bottomInset -
reservedTop;
if (!availableHeight.isFinite || availableHeight <= 0) {
return mediaQuery.size.height;
}
return availableHeight.clamp(0.0, mediaQuery.size.height).toDouble();
}
double clampRoomGameHeight(
double value,
double maxHeight, {
double minHeight = 0,
}) {
if (!value.isFinite || value <= 0) {
return maxHeight > 0 ? maxHeight : 0;
}
if (!maxHeight.isFinite || maxHeight <= 0) {
return value;
}
if (minHeight > maxHeight) {
return maxHeight;
}
return value.clamp(minHeight, maxHeight).toDouble();
}
String resolveRoomGameScreenMode(
Map<String, dynamic> launchParams, {
required bool fullScreen,
}) {
final candidates = <String>[
launchParams['screenMode']?.toString() ?? '',
launchParams['screen_mode']?.toString() ?? '',
launchParams['displayMode']?.toString() ?? '',
launchParams['gameScreenMode']?.toString() ?? '',
];
for (final candidate in candidates) {
final trimmed = candidate.trim();
if (trimmed.isNotEmpty) {
return trimmed.toLowerCase();
}
}
return fullScreen ? 'full' : 'half';
}
Map<String, dynamic> buildRoomGameViewportPayload(
BuildContext context, {
required double containerHeight,
double? dialogHeight,
double headerHeight = 0,
double topCrop = 0,
required String screenMode,
required int safeHeight,
double designWidth = roomGameDesignWidth,
double? designHeight,
double? actualWebViewHeight,
}) {
final mediaQuery = MediaQuery.maybeOf(context);
final size = mediaQuery?.size ?? Size.zero;
final padding = mediaQuery?.padding ?? EdgeInsets.zero;
final viewInsets = mediaQuery?.viewInsets ?? EdgeInsets.zero;
final targetDesignHeight =
designHeight ?? (safeHeight > 0 ? safeHeight.toDouble() : null);
final expectedHeight =
targetDesignHeight != null && size.width > 0 && designWidth > 0
? size.width * targetDesignHeight / designWidth
: null;
final availableHeight = resolveRoomGameAvailableHeight(context);
return <String, dynamic>{
'containerWidth': _roundMetric(size.width),
'containerHeight': _roundMetric(containerHeight),
'visibleHeight': _roundMetric(containerHeight),
'webViewHeight': _roundMetric(actualWebViewHeight ?? containerHeight),
'dialogHeight': _roundMetric(
dialogHeight ?? containerHeight + headerHeight,
),
'screenHeight': _roundMetric(size.height),
'headerHeight': _roundMetric(headerHeight),
'topCrop': _roundMetric(topCrop),
'safeAreaTop': _roundMetric(padding.top),
'safeAreaBottom': _roundMetric(padding.bottom),
'viewInsetBottom': _roundMetric(viewInsets.bottom),
'availableHeight': _roundMetric(availableHeight),
'devicePixelRatio': _roundMetric(mediaQuery?.devicePixelRatio ?? 1),
'screenMode': screenMode,
'isCompactViewport':
expectedHeight != null ? containerHeight + 1 < expectedHeight : false,
'designWidth': _roundMetric(designWidth),
if (targetDesignHeight != null)
'designHeight': _roundMetric(targetDesignHeight),
'safeHeight': safeHeight,
};
}
Map<String, dynamic> mergeRoomGameViewportPayload(
Map<String, dynamic> payload,
Map<String, dynamic> viewport,
) {
if (viewport.isEmpty) {
return payload;
}
return <String, dynamic>{
...payload,
'viewport': viewport,
'nativeViewport': viewport,
'containerWidth': viewport['containerWidth'],
'containerHeight': viewport['containerHeight'],
'visibleHeight': viewport['visibleHeight'],
'webViewHeight': viewport['webViewHeight'],
'dialogHeight': viewport['dialogHeight'],
'safeAreaTop': viewport['safeAreaTop'],
'safeAreaBottom': viewport['safeAreaBottom'],
'devicePixelRatio': viewport['devicePixelRatio'],
'screenMode': viewport['screenMode'],
'isCompactViewport': viewport['isCompactViewport'],
};
}
double _roundMetric(double value) {
if (!value.isFinite) {
return 0;
}
return double.parse(value.toStringAsFixed(2));
}

View File

@ -11,6 +11,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/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/ui_kit/components/sc_tts.dart';
@ -33,7 +34,6 @@ class BaishunGamePage extends StatefulWidget {
class _BaishunGamePageState extends State<BaishunGamePage> {
static const String _logPrefix = '[BaishunGame]';
static const double _designWidth = 750;
final RoomGameRepository _repository = RoomGameRepository();
final Set<Factory<OneSequenceGestureRecognizer>> _webGestureRecognizers =
@ -260,15 +260,17 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
_hasDeliveredLaunchConfig = true;
final rawConfig = widget.launchModel.bridgeConfig;
final config = _buildEffectiveBridgeConfig(rawConfig);
final viewport = _buildViewportPayload();
_log(
'send_config jsCallback=${jsCallbackPath ?? ''} '
'force=$force config=${_stringifyForLog(_buildConfigSummary(config, rawConfig: rawConfig))}',
'force=$force config=${_stringifyForLog(<String, dynamic>{..._buildConfigSummary(config, rawConfig: rawConfig), 'viewport': viewport})}',
);
try {
await _controller.runJavaScript(
BaishunJsBridge.buildConfigScript(
config,
jsCallbackPath: jsCallbackPath,
viewportPayload: viewport,
),
);
} catch (error) {
@ -609,22 +611,45 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
return 0;
}
String _resolveScreenMode() {
return resolveRoomGameScreenMode(
widget.game.launchParams,
fullScreen: widget.game.fullScreen,
);
}
double _calculateWebViewHeight(BuildContext context) {
final screenSize = MediaQuery.sizeOf(context);
final safePadding = MediaQuery.paddingOf(context);
final mediaQuery = MediaQuery.maybeOf(context);
final screenSize = mediaQuery?.size ?? Size.zero;
final safeHeight = _resolveSafeHeight();
final fallbackHeight = screenSize.height * 0.5;
final maxHeight = resolveRoomGameAvailableHeight(
context,
reservedTop: 12.w,
);
if (safeHeight <= 0 || screenSize.width <= 0) {
return fallbackHeight;
return clampRoomGameHeight(fallbackHeight, maxHeight);
}
final ratio = _designWidth / safeHeight;
final ratio = roomGameDesignWidth / safeHeight;
final webViewHeight = screenSize.width / ratio;
final maxHeight = screenSize.height - safePadding.top - 12.w;
if (maxHeight <= 0) {
return fallbackHeight;
}
return webViewHeight.clamp(0.0, maxHeight).toDouble();
return clampRoomGameHeight(webViewHeight, maxHeight);
}
Map<String, dynamic> _buildViewportPayload() {
final topCrop = 28.w;
final webViewHeight = _calculateWebViewHeight(context);
final safeHeight = _resolveSafeHeight();
return buildRoomGameViewportPayload(
context,
containerHeight: webViewHeight,
dialogHeight: webViewHeight,
topCrop: topCrop,
screenMode: _resolveScreenMode(),
safeHeight: safeHeight,
designHeight: safeHeight > 0 ? safeHeight.toDouble() : null,
actualWebViewHeight: webViewHeight + topCrop,
);
}
@override

View File

@ -10,6 +10,7 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/modules/room_game/bridge/leader_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/ui_kit/components/sc_tts.dart';
@ -32,7 +33,6 @@ class LeaderGamePage extends StatefulWidget {
class _LeaderGamePageState extends State<LeaderGamePage> {
static const String _logPrefix = '[LeaderGame]';
static const double _designWidth = 750;
static const double _hdDesignHeight = 1044;
final RoomGameRepository _repository = RoomGameRepository();
@ -215,7 +215,9 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
}
try {
await _controller.runJavaScript(
LeaderJsBridge.bootstrapScript(launchPayload: _buildLaunchPayload()),
LeaderJsBridge.bootstrapScript(
launchPayload: _buildLaunchPayload(viewport: _buildViewportPayload()),
),
);
} catch (error) {
_log(
@ -328,23 +330,10 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
}
String _resolveScreenMode() {
final launchParams = widget.game.launchParams;
final candidates = <String>[
launchParams['screenMode']?.toString() ?? '',
launchParams['screen_mode']?.toString() ?? '',
launchParams['displayMode']?.toString() ?? '',
launchParams['gameScreenMode']?.toString() ?? '',
];
for (final candidate in candidates) {
final trimmed = candidate.trim();
if (trimmed.isNotEmpty) {
return trimmed.toLowerCase();
}
}
if (widget.game.fullScreen) {
return 'full';
}
return 'half';
return resolveRoomGameScreenMode(
widget.game.launchParams,
fullScreen: widget.game.fullScreen,
);
}
int _resolveSafeHeight() {
@ -357,33 +346,38 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
return 0;
}
double _calculateBodyHeight(BuildContext context) {
final screenSize = MediaQuery.sizeOf(context);
final safePadding = MediaQuery.paddingOf(context);
double _calculateBodyHeight(BuildContext context, double headerHeight) {
final mediaQuery = MediaQuery.maybeOf(context);
final screenSize = mediaQuery?.size ?? Size.zero;
final screenMode = _resolveScreenMode();
final safeHeight = _resolveSafeHeight();
final fallback = screenSize.width;
final maxHeight = screenSize.height - safePadding.top - 24.w;
if (maxHeight <= 0) {
return fallback;
final maxDialogHeight = resolveRoomGameAvailableHeight(
context,
reservedTop: 24.w,
);
final maxBodyHeight = maxDialogHeight - headerHeight;
if (maxBodyHeight <= 0) {
return 0;
}
if (screenMode == 'full') {
return maxHeight;
return maxBodyHeight;
}
if (safeHeight > 0 && screenSize.width > 0) {
final ratio = _designWidth / safeHeight;
final ratio = roomGameDesignWidth / safeHeight;
final bodyHeight = screenSize.width / ratio;
return bodyHeight.clamp(0.0, maxHeight).toDouble();
return clampRoomGameHeight(bodyHeight, maxBodyHeight);
}
if (screenMode == 'hd') {
final bodyHeight = screenSize.width * _hdDesignHeight / _designWidth;
return bodyHeight.clamp(0.0, maxHeight).toDouble();
final bodyHeight =
screenSize.width * _hdDesignHeight / roomGameDesignWidth;
return clampRoomGameHeight(bodyHeight, maxBodyHeight);
}
return fallback.clamp(0.0, maxHeight).toDouble();
return clampRoomGameHeight(fallback, maxBodyHeight);
}
void _log(String message) {
@ -446,7 +440,51 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
}
}
Map<String, dynamic> _buildLaunchPayload() {
Map<String, dynamic> _buildLaunchPayload({
Map<String, dynamic> viewport = const <String, dynamic>{},
}) {
final launchConfig = mergeRoomGameViewportPayload(
widget.launchModel.launchConfig.toJson(),
viewport,
);
final payload = <String, dynamic>{
'provider': widget.launchModel.provider,
'gameId': widget.launchModel.gameId,
'providerGameId': widget.launchModel.providerGameId,
'gameSessionId': widget.launchModel.gameSessionId,
'entry': widget.launchModel.entry.toJson(),
'launchConfig': launchConfig,
'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,
},
};
return mergeRoomGameViewportPayload(payload, viewport);
}
Map<String, dynamic> _buildViewportPayload() {
final headerHeight = 44.w;
final bodyHeight = _calculateBodyHeight(context, headerHeight);
final safeHeight = _resolveSafeHeight();
return buildRoomGameViewportPayload(
context,
containerHeight: bodyHeight,
dialogHeight: bodyHeight + headerHeight,
headerHeight: headerHeight,
screenMode: _resolveScreenMode(),
safeHeight: safeHeight,
designHeight:
safeHeight > 0
? safeHeight.toDouble()
: (_resolveScreenMode() == 'hd' ? _hdDesignHeight : null),
);
}
Map<String, dynamic> _buildLaunchPayloadForLog() {
return <String, dynamic>{
'provider': widget.launchModel.provider,
'gameId': widget.launchModel.gameId,
@ -466,7 +504,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
}
Map<String, dynamic> _buildLaunchSummary() {
final payload = _buildLaunchPayload();
final payload = _buildLaunchPayloadForLog();
return <String, dynamic>{
'roomId': widget.roomId,
'provider': widget.game.provider,
@ -525,8 +563,8 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
@override
Widget build(BuildContext context) {
final bodyHeight = _calculateBodyHeight(context);
final headerHeight = 44.w;
final bodyHeight = _calculateBodyHeight(context, headerHeight);
return PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) async {

View File

@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'dart:convert';
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_game_page.dart';
import 'package:yumi/modules/room_game/views/leader_game_page.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
@ -210,11 +211,17 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.maybeOf(context);
final screenHeight = mediaQuery?.size.height ?? ScreenUtil().screenHeight;
final sheetHeight = clampRoomGameHeight(
screenHeight * 0.5,
resolveRoomGameAvailableHeight(context),
);
return SafeArea(
top: false,
child: SizedBox(
width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight * 0.5,
height: sheetHeight,
child: LayoutBuilder(
builder: (context, constraints) {
final innerLeft = constraints.maxWidth * 0.05;

View File

@ -46,6 +46,10 @@ import '../../ui_kit/components/sc_float_ichart.dart';
typedef OnSoundVoiceChange = Function(num index, int volum);
typedef RtcProvider = RealTimeCommunicationManager;
enum RoomStartupStatus { idle, loading, ready, failed }
enum RoomStartupFailureType { none, entry, im, rtc }
class RealTimeCommunicationManager extends ChangeNotifier {
static const Duration _micListPollingInterval = Duration(seconds: 2);
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
@ -57,6 +61,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false;
bool _isExitingCurrentVoiceRoomSession = false;
RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle;
RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none;
int _roomStartupFailureToken = 0;
bool _isHandlingRoomStartupFailure = false;
Timer? _micListPollingTimer;
Timer? _onlineUsersPollingTimer;
bool _isRefreshingMicList = false;
@ -129,6 +137,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool get isExitingCurrentVoiceRoomSession =>
_isExitingCurrentVoiceRoomSession;
RoomStartupStatus get roomStartupStatus => _roomStartupStatus;
RoomStartupFailureType get roomStartupFailureType => _roomStartupFailureType;
int get roomStartupFailureToken => _roomStartupFailureToken;
bool get shouldShowRoomVisualEffects =>
currenRoom != null && _roomVisualEffectsEnabled;
@ -140,6 +154,35 @@ class RealTimeCommunicationManager extends ChangeNotifier {
notifyListeners();
}
void _setRoomStartupLoading() {
_isHandlingRoomStartupFailure = false;
_roomStartupFailureType = RoomStartupFailureType.none;
if (_roomStartupStatus == RoomStartupStatus.loading) {
return;
}
_roomStartupStatus = RoomStartupStatus.loading;
notifyListeners();
}
void _setRoomStartupReady() {
_isHandlingRoomStartupFailure = false;
_roomStartupFailureType = RoomStartupFailureType.none;
if (_roomStartupStatus == RoomStartupStatus.ready) {
return;
}
_roomStartupStatus = RoomStartupStatus.ready;
notifyListeners();
}
void _setRoomStartupFailed(RoomStartupFailureType failureType) {
_stopRoomStatePolling();
_isHandlingRoomStartupFailure = false;
_roomStartupFailureType = failureType;
_roomStartupFailureToken += 1;
_roomStartupStatus = RoomStartupStatus.failed;
notifyListeners();
}
void _setExitingCurrentVoiceRoomSession(bool enabled, {bool notify = true}) {
if (_isExitingCurrentVoiceRoomSession == enabled) {
return;
@ -775,7 +818,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
});
}
Future<void> joinAgoraVoiceChannel() async {
Future<void> joinAgoraVoiceChannel({bool throwOnError = false}) async {
try {
final rtcEngine = await _initAgoraRtcEngine();
rtcEngine.setAudioProfile(
@ -814,6 +857,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_syncSelfMicRuntimeState();
rtcEngine.muteAllRemoteAudioStreams(roomIsMute);
} catch (e) {
if (throwOnError) {
rethrow;
}
SCTts.show("Join room fail");
exitCurrentVoiceRoomSession(false);
print('加入失败:${e.runtimeType},${e.toString()}');
@ -1046,6 +1092,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
retrieveMicrophoneList();
fetchOnlineUsersList();
_startRoomStatePolling();
_setRoomStartupReady();
setRoomVisualEffectsEnabled(true);
SCFloatIchart().remove();
SCNavigatorUtils.push(
@ -1099,7 +1146,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
///
Future initializeRoomSession({
Future<void> initializeRoomSession({
bool needOpenRedenvelope = false,
String? redPackId,
}) async {
@ -1112,7 +1159,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name) {
///,
VoiceRoomRoute.openRoomEdit(context!, needRestCurrentRoomInfo: true);
return Future;
return;
}
SCRoomUtils.roomSCGlobalConfig(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
@ -1126,46 +1173,57 @@ class RealTimeCommunicationManager extends ChangeNotifier {
notifyListeners();
});
}
_setRoomStartupLoading();
setRoomVisualEffectsEnabled(true);
SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false);
var joinResult = await rtmProvider?.joinRoomGroup(
currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
"",
);
SCChatRoomRepository()
.rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "")
.catchError((e) {
return true; //
});
rtmProvider?.addMsg(
Msg(
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
msg: SCAppLocalizations.of(context!)!.systemRoomTips,
type: SCRoomMsgType.systemTips,
),
);
rtmProvider?.addMsg(
Msg(
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
msg: currenRoom?.roomProfile?.roomProfile?.roomDesc,
type: SCRoomMsgType.systemTips,
),
);
unawaited(_bootstrapEnteredVoiceRoomSession());
}
await _bootstrapRoomHeartbeatState();
Future<void> _bootstrapEnteredVoiceRoomSession() async {
var failureType = RoomStartupFailureType.im;
try {
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
final joinResult = await rtmProvider?.joinRoomGroup(groupId, "");
if (joinResult == null || joinResult.code != 0) {
debugPrint(
'[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}',
);
_setRoomStartupFailed(RoomStartupFailureType.im);
return;
}
rtmProvider?.addMsg(
Msg(
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
msg: SCAppLocalizations.of(context!)!.systemRoomTips,
type: SCRoomMsgType.systemTips,
),
);
rtmProvider?.addMsg(
Msg(
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
msg: currenRoom?.roomProfile?.roomProfile?.roomDesc,
type: SCRoomMsgType.systemTips,
),
);
await _bootstrapRoomHeartbeatState();
///
retrieveMicrophoneList();
fetchOnlineUsersList();
_startRoomStatePolling();
Provider.of<SocialChatRoomManager>(
context!,
listen: false,
).fetchContributionLevelData(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
);
failureType = RoomStartupFailureType.rtc;
await joinAgoraVoiceChannel(throwOnError: true);
_setRoomStartupReady();
///
retrieveMicrophoneList();
fetchOnlineUsersList();
_startRoomStatePolling();
Provider.of<SocialChatRoomManager>(
context!,
listen: false,
).fetchContributionLevelData(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
);
await joinAgoraVoiceChannel();
if (joinResult?.code == 0) {
Msg joinMsg = Msg(
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
user: AccountStorage().getCurrentUser()?.userProfile,
@ -1174,28 +1232,44 @@ class RealTimeCommunicationManager extends ChangeNotifier {
type: SCRoomMsgType.joinRoom,
);
rtmProvider?.dispatchMessage(joinMsg, addLocal: true);
if (SCGlobalConfig.isEntryVehicleAnimation) {
if (AccountStorage().getCurrentUser()?.userProfile?.getMountains() !=
null &&
shouldShowRoomVisualEffects) {
Future.delayed(Duration(milliseconds: 550), () {
SCGiftVapSvgaManager().play(
AccountStorage()
.getCurrentUser()
?.userProfile
?.getMountains()
?.sourceUrl ??
"",
priority: 100,
type: 1,
);
});
}
}
if (rtmProvider?.msgUserJoinListener != null) {
rtmProvider?.msgUserJoinListener!(joinMsg);
_playRoomEntryEffects(joinMsg);
_loadRoomSecondaryData();
} catch (error, stackTrace) {
debugPrint('[RoomStartup] bootstrap failed: $error\n$stackTrace');
_setRoomStartupFailed(failureType);
}
}
void _playRoomEntryEffects(Msg joinMsg) {
if (SCGlobalConfig.isEntryVehicleAnimation) {
if (AccountStorage().getCurrentUser()?.userProfile?.getMountains() !=
null &&
shouldShowRoomVisualEffects) {
Future.delayed(Duration(milliseconds: 550), () {
SCGiftVapSvgaManager().play(
AccountStorage()
.getCurrentUser()
?.userProfile
?.getMountains()
?.sourceUrl ??
"",
priority: 100,
type: 1,
);
});
}
}
if (rtmProvider?.msgUserJoinListener != null) {
rtmProvider?.msgUserJoinListener!(joinMsg);
}
}
void _loadRoomSecondaryData() {
SCChatRoomRepository()
.rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "")
.catchError((e) {
return true; //
});
SCChatRoomRepository()
.rocketStatus(currenRoom?.roomProfile?.roomProfile?.id ?? "")
.then((res) {
@ -1391,6 +1465,45 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
}
Future<void> confirmRoomStartupFailureAndLeave({
BuildContext? navigationContext,
}) async {
if (_isHandlingRoomStartupFailure) {
return;
}
_isHandlingRoomStartupFailure = true;
final failedRoom = currenRoom;
final groupId = failedRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
final roomId = failedRoom?.roomProfile?.roomProfile?.id ?? "";
final navigatorContext = navigationContext ?? context;
final navigator =
navigatorContext != null ? Navigator.maybeOf(navigatorContext) : null;
try {
_stopRoomStatePolling();
SCGiftVapSvgaManager().stopPlayback();
SCHeartbeatUtils.cancelTimer();
SCHeartbeatUtils.cancelAnchorTimer();
rtmProvider?.cleanRoomData();
if (groupId.isNotEmpty) {
await rtmProvider?.quitGroup(groupId);
}
await engine?.leaveChannel();
await Future.delayed(const Duration(milliseconds: 100));
if (roomId.isNotEmpty) {
await SCAccountRepository().quitRoom(roomId);
}
} catch (e) {
print('rtc进房失败清理本地房间状态出错: $e');
} finally {
_clearData();
_isHandlingRoomStartupFailure = false;
notifyListeners();
if (navigator != null && navigator.mounted && navigator.canPop()) {
navigator.pop();
}
}
}
///
void _clearData() {
_stopRoomStatePolling();
@ -1399,6 +1512,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_disableMicListRefreshForCurrentSession = false;
_disableOnlineUsersRefreshForCurrentSession = false;
_isSelfMicActionInFlight = false;
_roomStartupStatus = RoomStartupStatus.idle;
_roomStartupFailureType = RoomStartupFailureType.none;
_isHandlingRoomStartupFailure = false;
_pendingMicListRefresh = false;
_pendingMicListRefreshNotifyIfUnchanged = false;
_seatEmojiEventVersions.clear();

View File

@ -5,6 +5,7 @@ import 'package:yumi/shared/tools/sc_url_launcher_utils.dart';
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import '../../shared/data_sources/models/enum/sc_banner_open_type.dart';
@ -35,12 +36,29 @@ class SCBannerUtils {
}
if (SCStringUtils.checkIfUrl(params)) {
final h5Url = _appendToken(params);
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(params)}&showTitle=false",
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(h5Url)}&showTitle=false",
replace: false,
);
}
}
}
static String _appendToken(String url) {
final token = AccountStorage().getToken();
if (token.isEmpty) {
return url;
}
final uri = Uri.tryParse(url);
if (uri == null) {
return url;
}
final queryParameters = Map<String, String>.from(uri.queryParameters);
queryParameters['token'] = token;
return uri.replace(queryParameters: queryParameters).toString();
}
}

View File

@ -1,5 +1,6 @@
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_debouncer/flutter_debouncer.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -288,7 +289,16 @@ class OpenRoomLoadingDialog extends Dialog {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image.asset("sc_images/raw/loading.webp", width: 80.w),
SizedBox(
width: 80.w,
height: 80.w,
child: Center(
child: CupertinoActivityIndicator(
color: Colors.white24,
radius: 14.w,
),
),
),
SizedBox(height: 12.w),
Text(
"正在进入聊天室......",

View File

@ -3,6 +3,13 @@
## 当前总目标
- 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。
## 本轮语言房进房加载逻辑优化(已完成)
- 已按竞品加载思路梳理当前语言房进房链路:当前业务 `entryRoom` 成功后会先进入房间页IM 加群、声网入会、麦位与在线用户刷新在房间页展示后继续进行这与“IM 和声网都完成后才进入完整房间”的体验存在差异。
- 已把语言房进房过程改造为“先进入房间骨架页,再完成关键启动任务”的状态机:新增房间启动态,进入房间页后显示房内 loadingIM 加群和声网入会成功后才切到 ready任一关键步骤失败则进入 failed。
- 已将 IM 加群失败和声网入会失败统一收口到房间页阻断弹窗:失败后不再分散 toast 或静默失败,用户点击确认后会清理 IM 群、声网频道、心跳、轮询与本地房间状态,并返回上一级页面。
- 已为进房失败与语音连接失败提示补齐 `en/ar/tr/bn` 多语言文案,避免新失败弹窗出现硬编码中文。
- 当前麦位与在线用户仍保持分批加载策略:进入房间页后会继续拉麦位、在线用户并启动轮询,头像框、礼物、火箭、红包、榜单等非关键资源仍放在房间进入后的异步补齐链路,避免阻塞首屏骨架。
## 本轮语言房背景页框架(进行中)
- 已按 2026-04-22 当前最新需求先落第一版页面骨架:新增两个独立路由,分别用于“背景选择页”和“自定义背景上传页”;其中语音房底部菜单里的 `Background` 图标入口,现已从旧的房间主题页切到新的背景选择页。
- 背景选择页已按“接近项目商店页”的结构先搭好导航和内容骨架:顶部为返回键、`官方 / 我的` 两个 tab 和右侧 `自定义` 文本按钮;点击右侧 `自定义` 会跳转到新的上传页。当前页内已先补上双列背景卡片栅格、选中高亮和“使用中”状态角标,便于后续继续接后台数据。