修改了一部分

This commit is contained in:
roxy 2026-05-15 22:49:21 +08:00
parent ea296e461b
commit 51083ef57f
11 changed files with 560 additions and 64 deletions

View File

@ -66,15 +66,10 @@ class _SCIndexPageState extends State<SCIndexPage>
_initializePages();
unawaited(_loadTaskClaimableCount());
generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false);
Provider.of<RtcProvider>(
context,
listen: false,
).initializeRealTimeCommunicationManager(context);
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
rtcProvider.initializeRealTimeCommunicationManager(context);
unawaited(
Provider.of<RtcProvider>(
context,
listen: false,
).prewarmRtcEngine().catchError((error, stackTrace) {}),
rtcProvider.prewarmRtcEngine().catchError((error, stackTrace) {}),
);
Provider.of<RtmProvider>(context, listen: false).init(context);
Provider.of<SocialChatUserProfileManager>(
@ -89,9 +84,7 @@ class _SCIndexPageState extends State<SCIndexPage>
WakelockPlus.enable();
_showEntryDialogs();
});
if (DataPersistence.getLastTimeRoomId().isNotEmpty) {
unawaited(DataPersistence.setLastTimeRoomId(""));
}
unawaited(rtcProvider.cleanupPersistedVoiceRoomSessionBeforeEntry());
}
@override

View File

@ -331,9 +331,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Timer? _onlineUsersPollingTimer;
Timer? _roomEntryEffectTimer;
Timer? _roomRocketLaunchAnimationTimer;
String? _lastRoomRocketLaunchNoticeKey;
Timer? _roomRedPacketPresenceTimer;
Future<void>? _rtcEnginePrewarmTask;
Future<void>? _pendingRoomSwitchRtcLeaveTask;
Future<void>? _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<void> 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<void> _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<void> _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<void> _retryExitNetworkRequest(
String name,
Future<void> 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();

View File

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String> _payloadStringList(dynamic value) {
if (value is Iterable) {
return value

View File

@ -78,7 +78,7 @@ abstract class SocialChatUserRepository {
Future anchorHeartbeat(String roomId);
///退
Future<bool> quitRoom(String roomId);
Future<bool> quitRoom(String roomId, {bool silentErrorToast = false});
///
Future<SCEditRoomInfoRes> editRoomInfo(

View File

@ -123,7 +123,7 @@ String _stringValue(dynamic value) {
}
String _roomCoverUrl(Map<String, dynamic> data) {
return _firstStringValue([
return _firstAssetString([
data['roomCoverUrl'],
data['roomCover'],
data['roomAvatar'],
@ -137,7 +137,7 @@ String _roomCoverUrl(Map<String, dynamic> data) {
}
String _rocketIconUrl(Map<String, dynamic> data) {
return _firstStringValue([
return _firstAssetString([
data['rocket_icon_url'],
data['rocketIconUrl'],
data['rocketIcon'],
@ -159,7 +159,7 @@ String _rocketIconUrl(Map<String, dynamic> data) {
}
String _rocketAnimationUrl(Map<String, dynamic> data) {
return _firstStringValue([
return _firstAssetString([
data['rocket_animation_url'],
data['rocketAnimationUrl'],
data['rocketSvgaUrl'],
@ -167,28 +167,49 @@ String _rocketAnimationUrl(Map<String, dynamic> 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<dynamic> values) {
String _firstAssetString(Iterable<dynamic> 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<dynamic> 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;

View File

@ -195,6 +195,14 @@ class DataPersistence {
return getString("last_time_room_id");
}
static Future<bool> 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<bool> setPlayGiftMusic(bool muteMusic) async {
return await setBool("play_gift_music", muteMusic);
}

View File

@ -229,10 +229,17 @@ class SCAccountRepository implements SocialChatUserRepository {
///live/room/quit
@override
Future<bool> quitRoom(String roomId) async {
Future<bool> quitRoom(String roomId, {bool silentErrorToast = false}) async {
final result = await http.get<bool>(
"3e451c31ab651a577a3b7796b32041e7",
queryParams: {"roomId": roomId},
extra:
silentErrorToast
? const {
BaseNetworkClient.silentErrorToastKey: true,
BaseNetworkClient.suppressAuthLogoutKey: true,
}
: null,
fromJson: (json) => json as bool,
);
return result;

View File

@ -31,9 +31,9 @@ class SCChatRoomHelper {
try {
final currentRoomId =
Provider.of<RealTimeCommunicationManager>(
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 {

View File

@ -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 =

View File

@ -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');
});
}

View File

@ -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` 无 errorTRTC 迁移单测 `flutter test test/room_rtc_types_test.dart test/trtc_room_rtc_engine_adapter_test.dart` 通过。
---