From 51083ef57f034e66050218fc6cc52a902124957e Mon Sep 17 00:00:00 2001 From: roxy Date: Fri, 15 May 2026 22:49:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E4=B8=80=E9=83=A8?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/index/index_page.dart | 15 +- lib/services/audio/rtc_manager.dart | 235 +++++++++++++++++- lib/services/audio/rtm_manager.dart | 187 +++++++++++++- .../repositories/user_repository.dart | 2 +- .../room_rocket_launch_broadcast_message.dart | 61 ++++- .../sources/local/data_persistence.dart | 8 + .../repositories/sc_user_repository_impl.dart | 9 +- lib/shared/tools/sc_room_utils.dart | 9 +- .../room/rocket/room_rocket_dialog.dart | 75 +++--- test/room_rocket_pag_resource_test.dart | 21 ++ 需求进度.md | 2 + 11 files changed, 560 insertions(+), 64 deletions(-) diff --git a/lib/modules/index/index_page.dart b/lib/modules/index/index_page.dart index b7e06a2..6533fa4 100644 --- a/lib/modules/index/index_page.dart +++ b/lib/modules/index/index_page.dart @@ -66,15 +66,10 @@ class _SCIndexPageState extends State _initializePages(); unawaited(_loadTaskClaimableCount()); generalProvider = Provider.of(context, listen: false); - Provider.of( - context, - listen: false, - ).initializeRealTimeCommunicationManager(context); + final rtcProvider = Provider.of(context, listen: false); + rtcProvider.initializeRealTimeCommunicationManager(context); unawaited( - Provider.of( - context, - listen: false, - ).prewarmRtcEngine().catchError((error, stackTrace) {}), + rtcProvider.prewarmRtcEngine().catchError((error, stackTrace) {}), ); Provider.of(context, listen: false).init(context); Provider.of( @@ -89,9 +84,7 @@ class _SCIndexPageState extends State WakelockPlus.enable(); _showEntryDialogs(); }); - if (DataPersistence.getLastTimeRoomId().isNotEmpty) { - unawaited(DataPersistence.setLastTimeRoomId("")); - } + unawaited(rtcProvider.cleanupPersistedVoiceRoomSessionBeforeEntry()); } @override diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index bff0250..0461cf6 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -331,9 +331,11 @@ class RealTimeCommunicationManager extends ChangeNotifier { Timer? _onlineUsersPollingTimer; Timer? _roomEntryEffectTimer; Timer? _roomRocketLaunchAnimationTimer; + String? _lastRoomRocketLaunchNoticeKey; Timer? _roomRedPacketPresenceTimer; Future? _rtcEnginePrewarmTask; Future? _pendingRoomSwitchRtcLeaveTask; + Future? _persistedRoomCleanupTask; bool _isRefreshingMicList = false; bool _isRefreshingOnlineUsers = false; bool _disableMicListRefreshForCurrentSession = false; @@ -1721,6 +1723,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (!context.mounted) { return; } + await cleanupPersistedVoiceRoomSessionBeforeEntry(targetRoomId: roomId); + if (!context.mounted) { + return; + } if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { if (_currentRoomIsEntryPreview || _roomStartupStatus == RoomStartupStatus.loading) { @@ -2372,12 +2378,83 @@ class RealTimeCommunicationManager extends ChangeNotifier { return; } await _retryExitNetworkRequest('quit stale entered room $staleRoomId', () { - return SCAccountRepository().quitRoom(staleRoomId).then((didQuit) { + return SCAccountRepository() + .quitRoom(staleRoomId, silentErrorToast: true) + .then((didQuit) { + if (!didQuit) { + throw StateError('quitRoom returned false'); + } + }); + }); + } + + Future cleanupPersistedVoiceRoomSessionBeforeEntry({ + String? targetRoomId, + }) { + final runningTask = _persistedRoomCleanupTask; + if (runningTask != null) { + return runningTask; + } + final task = _cleanupPersistedVoiceRoomSessionBeforeEntry( + targetRoomId: targetRoomId, + ); + _persistedRoomCleanupTask = task; + return task.whenComplete(() { + if (identical(_persistedRoomCleanupTask, task)) { + _persistedRoomCleanupTask = null; + } + }); + } + + Future _cleanupPersistedVoiceRoomSessionBeforeEntry({ + String? targetRoomId, + }) async { + final staleRoomId = DataPersistence.getLastTimeRoomId().trim(); + if (staleRoomId.isEmpty) { + return; + } + + final currentRoomId = + (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); + if (currentRoomId.isNotEmpty) { + return; + } + + final currentUserId = + (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); + if (currentUserId.isEmpty || AccountStorage().getToken().trim().isEmpty) { + return; + } + + final staleRoomUserId = DataPersistence.getLastTimeRoomUserId().trim(); + if (staleRoomUserId.isNotEmpty && staleRoomUserId != currentUserId) { + await _clearPersistedRoomMarkerIfMatches(staleRoomId); + return; + } + + var didClean = false; + await _retryExitNetworkRequest( + 'quit persisted room $staleRoomId', + () async { + final didQuit = await SCAccountRepository().quitRoom( + staleRoomId, + silentErrorToast: true, + ); if (!didQuit) { throw StateError('quitRoom returned false'); } - }); - }); + didClean = true; + }, + ); + if (didClean) { + await _clearPersistedRoomMarkerIfMatches(staleRoomId); + if ((targetRoomId ?? "").trim() != staleRoomId) { + await SCHeartbeatUtils.scheduleHeartbeat( + SCHeartbeatStatus.ONLINE.name, + false, + ); + } + } } ///初始化房间相关 @@ -2694,8 +2771,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { ///更新房间火箭信息 void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) { + final previousStatus = roomRocketStatus; roomRocketStatus = res; notifyListeners(); + _publishRoomRocketLaunchNoticeIfNeeded(previousStatus, res); } ///刷新房间火箭信息 @@ -2710,8 +2789,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { return; } + final previousStatus = roomRocketStatus; roomRocketStatus = res; notifyListeners(); + _publishRoomRocketLaunchNoticeIfNeeded(previousStatus, res); } void handleRoomRocketLaunchBroadcast( @@ -2759,6 +2840,114 @@ class RealTimeCommunicationManager extends ChangeNotifier { return null; } + void _publishRoomRocketLaunchNoticeIfNeeded( + SCRoomRocketStatusRes? previous, + SCRoomRocketStatusRes current, + ) { + if (!_shouldPublishRoomRocketLaunchNotice(previous, current)) { + return; + } + final roomId = + (current.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "") + .trim(); + final groupId = + (currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "").trim(); + if (roomId.isEmpty || groupId.isEmpty) { + return; + } + final level = _roomRocketNoticeLevel(current); + final noticeKey = _roomRocketLaunchNoticeKey( + roomId: roomId, + level: level, + roundNo: current.roundNo ?? 0, + ); + if (_lastRoomRocketLaunchNoticeKey == noticeKey) { + return; + } + _lastRoomRocketLaunchNoticeKey = noticeKey; + rtmProvider?.addMsg( + Msg( + groupId: groupId, + msg: _roomRocketLaunchNoticeSeconds(current).toString(), + type: SCRoomMsgType.rocketLaunchNotice, + number: _roomRocketLaunchNoticeSeconds(current), + rocketIconUrl: _rocketIconUrlForStatus(current, level), + giftBatchId: noticeKey, + ), + ); + } + + bool _shouldPublishRoomRocketLaunchNotice( + SCRoomRocketStatusRes? previous, + SCRoomRocketStatusRes current, + ) { + if (previous == null) { + return false; + } + final currentRoomId = + (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); + if (currentRoomId.isNotEmpty && + (current.roomId ?? currentRoomId).trim() != currentRoomId) { + return false; + } + return _roomRocketStatusPercent(previous) < 99.5 && + _roomRocketStatusPercent(current) >= 99.5; + } + + double _roomRocketStatusPercent(SCRoomRocketStatusRes? status) { + if (status == null) { + return 0; + } + final value = status.displayPercent ?? status.energyPercent; + if (value != null) { + final raw = value.toDouble(); + final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; + return percent.clamp(0, 100).toDouble(); + } + final currentEnergy = status.currentEnergy ?? 0; + final maxEnergy = status.maxEnergy ?? status.needEnergy ?? 0; + if (maxEnergy <= 0) { + return 0; + } + return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); + } + + int _roomRocketNoticeLevel(SCRoomRocketStatusRes status) { + final level = status.currentLevel ?? status.level?.toInt() ?? 1; + return level.clamp(1, 99).toInt(); + } + + int _roomRocketLaunchNoticeSeconds(SCRoomRocketStatusRes status) { + final seconds = status.resetRemainingSeconds ?? 0; + if (seconds > 0 && seconds <= 60) { + return seconds; + } + return 10; + } + + String _rocketIconUrlForStatus(SCRoomRocketStatusRes status, int level) { + for (final item in status.levels ?? const []) { + if (item.level == level && item.rocketIconUrl.trim().isNotEmpty) { + return item.rocketIconUrl; + } + } + for (final item in roomRocketStatus?.levels ?? const []) { + if (item.level == level && item.rocketIconUrl.trim().isNotEmpty) { + return item.rocketIconUrl; + } + } + return ""; + } + + String _roomRocketLaunchNoticeKey({ + required String roomId, + required int level, + required int roundNo, + }) { + final roundKey = roundNo > 0 ? roundNo.toString() : 'full'; + return ['rocket_launch_notice', roomId, level, roundKey].join('|'); + } + void _playRoomRocketLaunchAnimation({ required String animationUrl, required RoomRocketLaunchBroadcastMessage launch, @@ -3066,7 +3255,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { exitRtmProvider?.cleanRoomData(); } catch (e) {} - _clearData(); + _clearData(clearPersistedRoomMarker: false); if (!isLogout && navigationContext != null) { VoiceRoomRoute.popVoiceRoomToPrevious(navigationContext); } @@ -3104,7 +3293,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { exitRtmProvider?.cleanRoomData(); } catch (e) {} - _clearData(); + _clearData(clearPersistedRoomMarker: false); final roomRtcLeaveTask = _leaveRoomRtcForExit(); _pendingRoomSwitchRtcLeaveTask = roomRtcLeaveTask; unawaited( @@ -3169,16 +3358,34 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (_isCurrentRoomId(roomId)) { return; } - final didQuit = await SCAccountRepository().quitRoom(roomId); + final didQuit = await SCAccountRepository().quitRoom( + roomId, + silentErrorToast: true, + ); if (!didQuit) { throw StateError('quitRoom returned false'); } + await _clearPersistedRoomMarkerIfMatches(roomId); }), ); } await Future.wait(tasks); } + Future _clearPersistedRoomMarkerIfMatches(String roomId) async { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return; + } + if (DataPersistence.getLastTimeRoomId().trim() != normalizedRoomId) { + return; + } + await Future.wait([ + DataPersistence.setLastTimeRoomId(""), + DataPersistence.setLastTimeRoomUserId(""), + ]); + } + Future _retryExitNetworkRequest( String name, Future Function() action, @@ -3257,7 +3464,13 @@ class RealTimeCommunicationManager extends ChangeNotifier { await Future.delayed(const Duration(milliseconds: 100)); if (!isPreviewOnlyFailure && roomId.isNotEmpty) { await SCChatRoomRepository().roomRedPacketPresenceLeave(roomId); - await SCAccountRepository().quitRoom(roomId); + final didQuit = await SCAccountRepository().quitRoom( + roomId, + silentErrorToast: true, + ); + if (didQuit) { + await _clearPersistedRoomMarkerIfMatches(roomId); + } } } catch (e) { } finally { @@ -3271,13 +3484,14 @@ class RealTimeCommunicationManager extends ChangeNotifier { } ///清空列表数据 - void _clearData() { + void _clearData({bool clearPersistedRoomMarker = true}) { _roomEntryRequestSerial += 1; _stopVoiceRoomForegroundService(); _roomEntryEffectTimer?.cancel(); _roomEntryEffectTimer = null; _roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer = null; + _lastRoomRocketLaunchNoticeKey = null; _stopRoomRedPacketPresenceHeartbeat(); _previewRoomSeatCount = null; _finishRoomStartupSeatLoading(executePendingAction: false); @@ -3320,7 +3534,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { _isExitingCurrentVoiceRoomSession = false; isMic = true; isMusicPlaying = false; - DataPersistence.setLastTimeRoomId(""); + if (clearPersistedRoomMarker) { + DataPersistence.setLastTimeRoomId(""); + DataPersistence.setLastTimeRoomUserId(""); + } SCGiftVapSvgaManager().stopPlayback(); SCRoomUtils.closeAllDialogs(); SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index f224c60..f434e6a 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -1862,6 +1862,36 @@ class RealTimeMessagingManager extends ChangeNotifier { return value.toString().trim(); } + String _payloadAssetText(dynamic value) { + if (value == null) { + return ''; + } + if (value is Map) { + final data = _broadcastPayloadMap(value); + return _firstNonBlank([ + _payloadAssetText(data['sourceUrl']), + _payloadAssetText(data['url']), + _payloadAssetText(data['resourceUrl']), + _payloadAssetText(data['fileUrl']), + _payloadAssetText(data['src']), + _payloadAssetText(data['path']), + _payloadAssetText(data['cover']), + _payloadAssetText(data['imageUrl']), + _payloadAssetText(data['svgUrl']), + _payloadAssetText(data['svg']), + _payloadAssetText(data['value']), + ]); + } + if (value is Iterable && value is! String) { + return _firstNonBlank(value.map(_payloadAssetText)); + } + final text = _payloadText(value); + if (text == '{}' || text == '[]' || text.toLowerCase() == 'null') { + return ''; + } + return text; + } + num? _payloadNum(dynamic value) { if (value is num) { return value; @@ -1886,6 +1916,11 @@ class RealTimeMessagingManager extends ChangeNotifier { currentContext, listen: false, ); + _publishRoomRocketLaunchNoticeFromStatusPayload( + rtcProvider: rtcProvider, + payload: data, + roomId: roomId, + ); unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId)); } @@ -2047,15 +2082,155 @@ class RealTimeMessagingManager extends ChangeNotifier { launch.rocketIconUrl, _rocketIconUrlForLevel(launch.safeLevel), ]), - giftBatchId: [ - 'rocket_launch_notice', - launch.launchNo, - launch.roomId, - launch.safeLevel, - ].join('|'), + giftBatchId: _roomRocketLaunchNoticeKey(launch), ); } + String _roomRocketLaunchNoticeKey(RoomRocketLaunchBroadcastMessage launch) { + return _roomRocketLaunchNoticeKeyForValues( + roomId: launch.roomId, + level: launch.safeLevel, + roundNo: launch.roundNo, + launchNo: launch.launchNo, + ); + } + + void _publishRoomRocketLaunchNoticeFromStatusPayload({ + required RealTimeCommunicationManager rtcProvider, + required Map payload, + required String roomId, + }) { + final previousPercent = _roomRocketStatusPercent( + rtcProvider.roomRocketStatus, + ); + final currentPercent = _roomRocketPayloadPercent(payload); + if (previousPercent >= 99.5 || currentPercent < 99.5) { + return; + } + final groupId = + (rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? '') + .trim(); + if (groupId.isEmpty) { + return; + } + final level = _roomRocketPayloadLevel( + payload, + rtcProvider.roomRocketStatus?.currentLevel ?? + rtcProvider.roomRocketStatus?.level?.toInt() ?? + 1, + ); + addMsg( + Msg( + groupId: groupId, + msg: _roomRocketPayloadNoticeSeconds(payload).toString(), + type: SCRoomMsgType.rocketLaunchNotice, + number: _roomRocketPayloadNoticeSeconds(payload), + rocketIconUrl: _firstNonBlank([ + _rocketIconFromPayload(payload), + _rocketIconUrlForLevel(level), + ]), + giftBatchId: _roomRocketLaunchNoticeKeyForValues( + roomId: roomId, + level: level, + roundNo: _payloadNum(payload['roundNo'])?.toInt() ?? 0, + launchNo: _payloadText(payload['launchNo']), + ), + ), + ); + } + + double _roomRocketStatusPercent(dynamic status) { + if (status == null) { + return 0; + } + final value = status.displayPercent ?? status.energyPercent; + if (value is num) { + final raw = value.toDouble(); + final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; + return percent.clamp(0, 100).toDouble(); + } + final currentEnergy = status.currentEnergy; + final maxEnergy = status.maxEnergy ?? status.needEnergy; + if (currentEnergy is! num || maxEnergy is! num || maxEnergy <= 0) { + return 0; + } + return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); + } + + double _roomRocketPayloadPercent(Map payload) { + final value = _payloadNum( + payload['displayPercent'] ?? + payload['energyPercent'] ?? + payload['percent'], + ); + if (value != null) { + final raw = value.toDouble(); + final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; + return percent.clamp(0, 100).toDouble(); + } + final currentEnergy = _payloadNum(payload['currentEnergy']); + final maxEnergy = _payloadNum( + payload['maxEnergy'] ?? payload['needEnergy'], + ); + if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) { + return 0; + } + return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); + } + + int _roomRocketPayloadLevel(Map payload, int fallback) { + final value = _payloadNum( + payload['currentLevel'] ?? + payload['level'] ?? + payload['rocketLevel'] ?? + payload['fromLevel'], + ); + return (value?.toInt() ?? fallback).clamp(1, 99).toInt(); + } + + int _roomRocketPayloadNoticeSeconds(Map payload) { + final seconds = + _payloadNum( + payload['countdownSeconds'] ?? + payload['launchDelaySeconds'] ?? + payload['delaySeconds'] ?? + payload['durationSeconds'], + )?.toInt() ?? + 10; + if (seconds > 0 && seconds <= 60) { + return seconds; + } + return 10; + } + + String _rocketIconFromPayload(Map payload) { + return _firstNonBlank([ + _payloadAssetText(payload['rocketIconUrl']), + _payloadAssetText(payload['rocketIcon']), + _payloadAssetText(payload['rocketUrl']), + _payloadAssetText(payload['rocketImageUrl']), + _payloadAssetText(payload['iconUrl']), + ]); + } + + String _roomRocketLaunchNoticeKeyForValues({ + required String roomId, + required int level, + required int roundNo, + String launchNo = '', + }) { + final roundKey = + roundNo > 0 + ? roundNo.toString() + : (launchNo.trim().isNotEmpty ? launchNo : 'full'); + return [ + 'rocket_launch_notice', + roomId, + level.clamp(1, 99).toInt(), + roundKey, + ].join('|'); + } + List _payloadStringList(dynamic value) { if (value is Iterable) { return value diff --git a/lib/shared/business_logic/repositories/user_repository.dart b/lib/shared/business_logic/repositories/user_repository.dart index 730b900..b6725c7 100644 --- a/lib/shared/business_logic/repositories/user_repository.dart +++ b/lib/shared/business_logic/repositories/user_repository.dart @@ -78,7 +78,7 @@ abstract class SocialChatUserRepository { Future anchorHeartbeat(String roomId); ///退出房间 - Future quitRoom(String roomId); + Future quitRoom(String roomId, {bool silentErrorToast = false}); ///修改房间信息 Future editRoomInfo( diff --git a/lib/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart b/lib/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart index 0ff31c4..b011b5d 100644 --- a/lib/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart +++ b/lib/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart @@ -123,7 +123,7 @@ String _stringValue(dynamic value) { } String _roomCoverUrl(Map data) { - return _firstStringValue([ + return _firstAssetString([ data['roomCoverUrl'], data['roomCover'], data['roomAvatar'], @@ -137,7 +137,7 @@ String _roomCoverUrl(Map data) { } String _rocketIconUrl(Map data) { - return _firstStringValue([ + return _firstAssetString([ data['rocket_icon_url'], data['rocketIconUrl'], data['rocketIcon'], @@ -159,7 +159,7 @@ String _rocketIconUrl(Map data) { } String _rocketAnimationUrl(Map data) { - return _firstStringValue([ + return _firstAssetString([ data['rocket_animation_url'], data['rocketAnimationUrl'], data['rocketSvgaUrl'], @@ -167,28 +167,49 @@ String _rocketAnimationUrl(Map data) { data['svgaUrl'], data['rocketLaunchSvgaUrl'], data['launchAnimationUrl'], + data['launchAnimation'], data['launchSvgaUrl'], + data['rocketAnimation'], + data['animation'], + data['animationResource'], + data['rocketAnimationResource'], + data['launchAnimationResource'], data['sourceUrl'], _asMap(data['rocket'])['rocket_animation_url'], _asMap(data['rocket'])['rocketAnimationUrl'], _asMap(data['rocket'])['rocketSvgaUrl'], _asMap(data['rocket'])['animationUrl'], + _asMap(data['rocket'])['rocketAnimation'], + _asMap(data['rocket'])['animation'], + _asMap(data['rocket'])['animationResource'], _asMap(data['rocket'])['svgaUrl'], _asMap(data['levelInfo'])['rocket_animation_url'], _asMap(data['levelInfo'])['rocketAnimationUrl'], _asMap(data['levelInfo'])['rocketSvgaUrl'], + _asMap(data['levelInfo'])['animationUrl'], + _asMap(data['levelInfo'])['rocketAnimation'], + _asMap(data['levelInfo'])['animation'], + _asMap(data['levelInfo'])['animationResource'], _asMap(data['rocketLevel'])['rocket_animation_url'], _asMap(data['rocketLevel'])['rocketAnimationUrl'], _asMap(data['rocketLevel'])['rocketSvgaUrl'], + _asMap(data['rocketLevel'])['animationUrl'], + _asMap(data['rocketLevel'])['rocketAnimation'], + _asMap(data['rocketLevel'])['animation'], + _asMap(data['rocketLevel'])['animationResource'], _asMap(data['config'])['rocket_animation_url'], _asMap(data['config'])['rocketAnimationUrl'], _asMap(data['config'])['rocketSvgaUrl'], + _asMap(data['config'])['animationUrl'], + _asMap(data['config'])['rocketAnimation'], + _asMap(data['config'])['animation'], + _asMap(data['config'])['animationResource'], ]); } -String _firstStringValue(Iterable values) { +String _firstAssetString(Iterable values) { for (final value in values) { - final text = _stringValue(value); + final text = _assetString(value); if (text.isNotEmpty) { return text; } @@ -196,6 +217,36 @@ String _firstStringValue(Iterable values) { return ''; } +String _assetString(dynamic value) { + if (value == null) { + return ''; + } + if (value is Map) { + final map = _asMap(value); + return _firstAssetString([ + map['sourceUrl'], + map['url'], + map['resourceUrl'], + map['fileUrl'], + map['src'], + map['path'], + map['cover'], + map['imageUrl'], + map['svgUrl'], + map['svg'], + map['value'], + ]); + } + if (value is Iterable && value is! String) { + return _firstAssetString(value); + } + final text = _stringValue(value); + if (text == '{}' || text == '[]' || text.toLowerCase() == 'null') { + return ''; + } + return text; +} + int _intValue(dynamic value, {int fallback = 0}) { if (value is int) { return value; diff --git a/lib/shared/data_sources/sources/local/data_persistence.dart b/lib/shared/data_sources/sources/local/data_persistence.dart index c99ad10..86c05c5 100644 --- a/lib/shared/data_sources/sources/local/data_persistence.dart +++ b/lib/shared/data_sources/sources/local/data_persistence.dart @@ -195,6 +195,14 @@ class DataPersistence { return getString("last_time_room_id"); } + static Future setLastTimeRoomUserId(String id) async { + return await setString("last_time_room_user_id", id); + } + + static String getLastTimeRoomUserId() { + return getString("last_time_room_user_id"); + } + static Future setPlayGiftMusic(bool muteMusic) async { return await setBool("play_gift_music", muteMusic); } diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 2226340..27ef037 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -229,10 +229,17 @@ class SCAccountRepository implements SocialChatUserRepository { ///live/room/quit @override - Future quitRoom(String roomId) async { + Future quitRoom(String roomId, {bool silentErrorToast = false}) async { final result = await http.get( "3e451c31ab651a577a3b7796b32041e7", queryParams: {"roomId": roomId}, + extra: + silentErrorToast + ? const { + BaseNetworkClient.silentErrorToastKey: true, + BaseNetworkClient.suppressAuthLogoutKey: true, + } + : null, fromJson: (json) => json as bool, ); return result; diff --git a/lib/shared/tools/sc_room_utils.dart b/lib/shared/tools/sc_room_utils.dart index d495f75..b6af09f 100644 --- a/lib/shared/tools/sc_room_utils.dart +++ b/lib/shared/tools/sc_room_utils.dart @@ -31,9 +31,9 @@ class SCChatRoomHelper { try { final currentRoomId = Provider.of( - context, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; return currentRoomId.isNotEmpty && currentRoomId == normalizedRoomId; } catch (_) { @@ -294,6 +294,9 @@ class SCChatRoomHelper { ), ); DataPersistence.setLastTimeRoomId(roomId); + DataPersistence.setLastTimeRoomUserId( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); } static void closeAllDialogs() async { diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart index 6e7a887..d42c660 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart @@ -788,7 +788,11 @@ class _RocketCanvas extends StatelessWidget { top: s(753), width: s(50), height: s(50), - child: _CrewAvatar(scale: scale, member: crew[index]), + child: _CrewAvatar( + scale: scale, + member: crew[index], + rank: index, + ), ), Positioned( left: s(315), @@ -1039,43 +1043,54 @@ class _LevelBadge extends StatelessWidget { } class _CrewAvatar extends StatelessWidget { - const _CrewAvatar({required this.scale, required this.member}); + const _CrewAvatar({ + required this.scale, + required this.member, + required this.rank, + }); final double scale; final RoomRocketCrewMember member; + final int rank; @override Widget build(BuildContext context) { return Stack( - alignment: Alignment.center, children: [ - Image.asset( - _RocketAssets.bottomAvatarRing, - width: 40 * scale, - height: 40 * scale, - fit: BoxFit.fill, - ), - ClipOval( - child: SizedBox( - width: 42 * scale, - height: 42 * scale, - child: - member.asset != null || member.imageUrl != null - ? _RocketImage( - asset: member.asset, - imageUrl: member.imageUrl, - fit: BoxFit.cover, - ) - : Image.asset( - _RocketAssets.defaultAvatar, - fit: BoxFit.cover, - gaplessPlayback: true, - ), - ), + Positioned( + left: 6.25 * scale, + top: 5 * scale, + width: 37.5 * scale, + height: 37.5 * scale, + child: ClipOval(child: _avatarImage()), ), + Positioned.fill(child: Image.asset(_frameAsset, fit: BoxFit.contain)), ], ); } + + String get _frameAsset { + return switch (rank) { + 0 => _RocketAssets.rankFrame1, + 1 => _RocketAssets.rankFrame2, + _ => _RocketAssets.rankFrame3, + }; + } + + Widget _avatarImage() { + if (member.asset != null || member.imageUrl != null) { + return _RocketImage( + asset: member.asset, + imageUrl: member.imageUrl, + fit: BoxFit.cover, + ); + } + return Image.asset( + _RocketAssets.defaultAvatar, + fit: BoxFit.cover, + gaplessPlayback: true, + ); + } } class _RocketImage extends StatelessWidget { @@ -1867,8 +1882,12 @@ class _RocketAssets { 'sc_images/room/rocket/bottom_strip_active.png'; static const String bottomTrophy = 'sc_images/room/rocket/bottom_trophy.png'; static const String backArrow = 'sc_images/room/rocket/back_arrow.png'; - static const String bottomAvatarRing = - 'sc_images/room/rocket/bottom_avatar_ring.png'; + static const String rankFrame1 = + 'sc_images/room/rocket/rocket_rank_frame_1.png'; + static const String rankFrame2 = + 'sc_images/room/rocket/rocket_rank_frame_2.png'; + static const String rankFrame3 = + 'sc_images/room/rocket/rocket_rank_frame_3.png'; static const String pillBg = 'sc_images/room/rocket/pill_bg.png'; static const String progressFull = 'sc_images/room/rocket/progress_full.png'; static const String progressEmpty = diff --git a/test/room_rocket_pag_resource_test.dart b/test/room_rocket_pag_resource_test.dart index 588ee74..0b102d4 100644 --- a/test/room_rocket_pag_resource_test.dart +++ b/test/room_rocket_pag_resource_test.dart @@ -1,6 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:yumi/app/config/app_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart'; void main() { @@ -58,4 +59,24 @@ void main() { expect(released, isTrue); timer?.cancel(); }); + + test('extracts launch pag urls from nested resource payloads', () { + final launch = RoomRocketLaunchBroadcastMessage.fromJson({ + 'roomId': 'room-1', + 'rocketAnimationResource': { + 'sourceUrl': 'https://cdn.example.com/rocket/launch.pag', + }, + 'rocketIcon': {'url': 'https://cdn.example.com/rocket/icon.png'}, + 'room': { + 'cover': {'url': 'https://cdn.example.com/room/cover.png'}, + }, + }); + + expect( + launch.rocketAnimationUrl, + 'https://cdn.example.com/rocket/launch.pag', + ); + expect(launch.rocketIconUrl, 'https://cdn.example.com/rocket/icon.png'); + expect(launch.roomCoverUrl, 'https://cdn.example.com/room/cover.png'); + }); } diff --git a/需求进度.md b/需求进度.md index 8412071..59076e1 100644 --- a/需求进度.md +++ b/需求进度.md @@ -13,6 +13,8 @@ - [x] 2026-05-15 Android 打包链路已继续收口:`pag-1.0.7` 原包缺 Android namespace、Kotlin JVM target 不一致且仍引用 Flutter v1 embedding,已切为本地 patched package 并补齐 `namespace/jvmTarget/v2 embedding` 兼容,避免火箭 PAG 接入后阻断 APK 构建。 - [x] 2026-05-15 已执行 `flutter build apk --debug`,结果通过,产物为 `build/app/outputs/flutter-apk/app-debug.apk`。 - [x] 2026-05-15 复跑迁移单测通过;同时修复火箭奖励记录模型 `rocketIconUrl` getter 缺失导致的测试编译错误。 +- [x] 2026-05-15 已修复杀进程后跨房间上麦残留:启动和进入新房间前不再直接清空 `last_time_room_id`,会先静默调用旧房间 `quitRoom`,成功后再清理本地标记,避免 A/B 房间同时展示同一用户麦位。 +- [x] 2026-05-15 已验证异常退出残留修复相关改动:定向 `flutter analyze --no-fatal-infos --no-fatal-warnings` 无 error,TRTC 迁移单测 `flutter test test/room_rtc_types_test.dart test/trtc_room_rtc_engine_adapter_test.dart` 通过。 ---