diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 2e7b39c..efb8533 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -21,6 +21,7 @@ import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/app/routes/sc_routes.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart'; @@ -60,6 +61,7 @@ class _PersonDetailPageState extends State { bool isLoading = true; bool isBlacklistLoading = true; bool isBlacklist = false; + bool _isBackgroundUpdating = false; Map counterMap = {}; List giftWallList = []; @@ -582,22 +584,25 @@ class _PersonDetailPageState extends State { ), items: backgroundPhotos.map((item) { - return GestureDetector( - child: netImage( + return _buildProfileBackgroundTapTarget( + ref, + netImage( url: item.url ?? "", width: ScreenUtil().screenWidth, height: 300.w, fit: BoxFit.cover, ), - onTap: () {}, ); }).toList(), ) - : Image.asset( - 'sc_images/person/sc_icon_profile_card_default_bg.png', - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, + : _buildProfileBackgroundTapTarget( + ref, + Image.asset( + 'sc_images/person/sc_icon_profile_card_default_bg.png', + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), ), ), Positioned( @@ -972,6 +977,79 @@ class _PersonDetailPageState extends State { ); } + Widget _buildProfileBackgroundTapTarget( + SocialChatUserProfileManager ref, + Widget child, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: + widget.isMe == "true" + ? () => _pickAndUpdateProfileBackground(ref) + : null, + child: child, + ); + } + + void _pickAndUpdateProfileBackground(SocialChatUserProfileManager ref) { + if (_isBackgroundUpdating) { + return; + } + SCPickUtils.pickImage( + context, + (bool success, String url) { + final imageUrl = url.trim(); + if (!success || imageUrl.isEmpty || !mounted) { + return; + } + _updateProfileBackground(ref, imageUrl); + }, + aspectRatio: ScreenUtil().screenWidth / 300.w, + backOriginalFile: false, + ); + } + + Future _updateProfileBackground( + SocialChatUserProfileManager ref, + String imageUrl, + ) async { + if (_isBackgroundUpdating) { + return; + } + _isBackgroundUpdating = true; + SCLoadingManager.show(context: context); + try { + final updatedProfile = await SCAccountRepository().updateUserInfo( + backgroundPhotos: [imageUrl], + ); + final returnedPhotos = updatedProfile.backgroundPhotos ?? []; + final hasUploadedPhoto = returnedPhotos.any( + (photo) => (photo.url ?? "").trim() == imageUrl, + ); + final backgroundPhotos = + hasUploadedPhoto + ? returnedPhotos + : [PersonPhoto(url: imageUrl, status: 1)]; + final mergedProfile = updatedProfile.copyWith( + backgroundPhotos: backgroundPhotos, + ); + ref.userProfile = mergedProfile; + ref.syncCurrentUserProfile(mergedProfile); + if (!mounted) { + return; + } + setState(() {}); + SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); + } catch (e) { + if (mounted) { + SCTts.show("update fail $e"); + } + } finally { + _isBackgroundUpdating = false; + SCLoadingManager.hide(); + } + } + Future _openChat() async { final conversation = V2TimConversation( type: ConversationType.V2TIM_C2C, diff --git a/lib/modules/user/profile/profile_wall_of_honors_page.dart b/lib/modules/user/profile/profile_wall_of_honors_page.dart index 6f2b724..8f34016 100644 --- a/lib/modules/user/profile/profile_wall_of_honors_page.dart +++ b/lib/modules/user/profile/profile_wall_of_honors_page.dart @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; class ProfileWallOfHonorsPage extends StatefulWidget { @@ -18,11 +23,55 @@ class _ProfileWallOfHonorsPageState extends State { static const Color _mutedText = Color(0xff8F9A98); int _selectedTabIndex = 0; - final int _honorCount = 0; - final int _eventCount = 0; + bool _loading = true; + SCUserBadgeRes _badges = SCUserBadgeRes.empty(); + List get _honorBadges => + _badges.displayBadges.where(_isHonorBadge).toList(); + + List get _eventBadges => + _badges.displayBadges.where(_isEventBadge).toList(); + + int get _honorCount => _honorBadges.length; + int get _eventCount => _eventBadges.length; int get _totalCount => _honorCount + _eventCount; + @override + void initState() { + super.initState(); + _loadBadges(); + } + + Future _loadBadges() async { + final userId = widget.tageId.trim(); + if (userId.isEmpty) { + setState(() { + _loading = false; + _badges = SCUserBadgeRes.empty(); + }); + return; + } + try { + final currentUserId = + AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; + final result = + currentUserId.isNotEmpty && currentUserId == userId + ? await SCAccountRepository().currentUserBadges() + : await SCAccountRepository().otherUserBadges(userId); + if (!mounted) return; + setState(() { + _badges = result; + _loading = false; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _badges = SCUserBadgeRes.empty(userId: userId); + _loading = false; + }); + } + } + Widget _buildTopBar() { return SizedBox( width: double.infinity, @@ -131,6 +180,106 @@ class _ProfileWallOfHonorsPageState extends State { ); } + Widget _buildLoadingState() { + return const SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: SizedBox( + width: 28, + height: 28, + child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), + ), + ), + ); + } + + Widget _buildBadgeGrid() { + final list = _selectedTabIndex == 0 ? _honorBadges : _eventBadges; + if (list.isEmpty) { + return _buildEmptyState(); + } + return SliverPadding( + padding: EdgeInsets.fromLTRB(16.w, 4.w, 16.w, 32.w), + sliver: SliverGrid( + delegate: SliverChildBuilderDelegate( + (context, index) => _buildBadgeItem(list[index]), + childCount: list.length, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisExtent: 132.w, + mainAxisSpacing: 14.w, + crossAxisSpacing: 12.w, + ), + ), + ); + } + + Widget _buildBadgeItem(WearBadge badge) { + final url = _badgeUrl(badge); + final name = (badge.badgeName ?? "").trim(); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 86.w, + height: 86.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0xff0E332D), + borderRadius: BorderRadius.circular(8.w), + border: Border.all( + color: const Color(0xff18F2B1).withValues(alpha: 0.24), + ), + ), + child: netImage( + url: url, + width: 68.w, + height: 68.w, + fit: BoxFit.contain, + noDefaultImg: true, + ), + ), + SizedBox(height: 8.w), + text( + name.isEmpty ? "--" : name, + fontSize: 12, + textColor: Colors.white, + fontWeight: FontWeight.w400, + maxLines: 2, + textAlign: TextAlign.center, + ), + ], + ); + } + + bool _isHonorBadge(WearBadge badge) { + final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; + final type = badge.type?.trim().toUpperCase() ?? ""; + return sourceType == "ACHIEVEMENT" || + sourceType == "VIP" || + (sourceType.isEmpty && + (type == "LONG" || type == "ACHIEVEMENT" || type == "VIP")); + } + + bool _isEventBadge(WearBadge badge) { + final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; + final type = badge.type?.trim().toUpperCase() ?? ""; + if (sourceType == "ACTIVITY") { + return true; + } + if (sourceType.isNotEmpty) { + return false; + } + return type == "ACTIVITY" || + type == "SHORT" || + (type != "LONG" && type != "VIP" && type != "ACHIEVEMENT"); + } + + String _badgeUrl(WearBadge badge) { + return (badge.selectUrl ?? "").trim(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -143,7 +292,7 @@ class _ProfileWallOfHonorsPageState extends State { SliverToBoxAdapter( child: Column(children: [_buildTopBar(), _buildTabs()]), ), - _buildEmptyState(), + _loading ? _buildLoadingState() : _buildBadgeGrid(), ], ), ), diff --git a/lib/services/audio/room_rtc_engine_adapter.dart b/lib/services/audio/room_rtc_engine_adapter.dart index c28111f..7115656 100644 --- a/lib/services/audio/room_rtc_engine_adapter.dart +++ b/lib/services/audio/room_rtc_engine_adapter.dart @@ -42,6 +42,8 @@ class RoomRtcAudioMixingConfig { abstract class RoomRtcEngineAdapter { RoomRtcEngineKind get kind; + bool get isInRoom; + Future prewarm(); Future enterRoomAsAudience(RoomRtcEnterRoomConfig config); diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index ec6ee84..c2a5041 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -345,7 +345,6 @@ class RealTimeCommunicationManager extends ChangeNotifier { Timer? _roomRocketLaunchAnimationTimer; Timer? _roomRocketGiftRefreshTimer; final List _roomRocketPostLaunchRefreshTimers = []; - String? _lastRoomRocketLaunchNoticeKey; Timer? _roomRedPacketPresenceTimer; Future? _rtcEnginePrewarmTask; Future? _pendingRoomSwitchRtcLeaveTask; @@ -1061,6 +1060,11 @@ class RealTimeCommunicationManager extends ChangeNotifier { _resetLocalAudioRuntimeTracking(); } + bool get _hasCurrentRoomRtcConnection { + final adapter = _roomRtcEngineAdapter; + return _roomRtcJoined && adapter != null && adapter.isInRoom; + } + bool get _canReportMicActive { return _roomRtcJoined && _roomRtcRoleIsBroadcaster && isOnMai(); } @@ -1507,6 +1511,42 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } + Future _rejoinCurrentRoomRtcForSameRoomReuse(String roomId) async { + if (_hasCurrentRoomRtcConnection) { + _setRoomStartupReady(); + return; + } + + try { + final trtcUserSig = await _fetchRoomRtcJoinCredentialForCurrentRoom(); + if (!_isCurrentRoomId(roomId)) { + return; + } + await joinRoomRtcVoiceChannel( + throwOnError: true, + trtcUserSig: trtcUserSig, + ); + if (!_isCurrentRoomId(roomId)) { + await _cleanupRoomRtcState( + clearMicSeats: currenRoom == null, + onlyIfBusinessRoomId: roomId, + ); + return; + } + await _bootstrapRoomHeartbeatState(); + if (!_isCurrentRoomId(roomId)) { + return; + } + _setRoomStartupReady(); + } catch (error) { + if (!_isCurrentRoomId(roomId)) { + return; + } + _stopVoiceRoomForegroundService(); + _setRoomStartupFailed(RoomStartupFailureType.rtc); + } + } + Future _switchRoomRtcToAudience() async { try { final switched = await _currentRoomRtcEngineAdapter.switchToAudience(); @@ -1779,12 +1819,20 @@ class RealTimeCommunicationManager extends ChangeNotifier { retrieveMicrophoneList(); fetchOnlineUsersList(); _startRoomStatePolling(); - _setRoomStartupReady(); + final shouldRejoinRoomRtc = !_hasCurrentRoomRtcConnection; + if (shouldRejoinRoomRtc) { + _setRoomStartupLoading(); + } else { + _setRoomStartupReady(); + } setRoomVisualEffectsEnabled(true); _startRoomRedPacketPresenceHeartbeat(roomId); _refreshRoomRedPacketListAfterEntry(roomId: roomId); _startVoiceRoomForegroundService(); VoiceRoomRoute.openVoiceRoom(context); + if (shouldRejoinRoomRtc) { + unawaited(_rejoinCurrentRoomRtcForSameRoomReuse(roomId)); + } } else { JoinRoomRes? preEnteredRoom; if (pwd == null && _previewRoomRequiresPassword(context, previewData)) { @@ -2823,10 +2871,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { ///更新房间火箭信息 void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) { - final previousStatus = roomRocketStatus; roomRocketStatus = res; notifyListeners(); - _publishRoomRocketLaunchNoticeIfNeeded(previousStatus, res); _scheduleRoomRocketAssetPreload( (res.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(), res, @@ -2845,10 +2891,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { return; } - final previousStatus = roomRocketStatus; roomRocketStatus = res; notifyListeners(); - _publishRoomRocketLaunchNoticeIfNeeded(previousStatus, res); _scheduleRoomRocketAssetPreload(resolvedRoomId, res); } @@ -2966,114 +3010,6 @@ 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, @@ -3680,7 +3616,6 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomRocketGiftRefreshTimer?.cancel(); _roomRocketGiftRefreshTimer = null; _cancelRoomRocketPostLaunchStatusRefresh(); - _lastRoomRocketLaunchNoticeKey = null; RoomRocketAssetPreloader.cancelCurrentRoom(); _stopRoomRedPacketPresenceHeartbeat(); _previewRoomSeatCount = null; diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 1f43643..11c77f2 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -121,6 +121,7 @@ class RealTimeMessagingManager extends ChangeNotifier { static const int _roomRocketLaunchRecentTtlMs = 90000; static const int _roomRocketLaunchLooseRecentTtlMs = 12000; static const int _roomRocketRegionBroadcastRecentTtlMs = 90000; + static const Duration _createRoomGroupTimeout = Duration(seconds: 12); static const List _roomRocketRewardPopupRetryDelays = [ Duration(milliseconds: 700), Duration(seconds: 2), @@ -654,11 +655,20 @@ class RealTimeMessagingManager extends ChangeNotifier { String groupID, String groupName, ) { - return V2TIMGroupManager().createGroup( - groupID: groupID, - groupType: GroupType.AVChatRoom, - groupName: groupName, - ); + return V2TIMGroupManager() + .createGroup( + groupID: groupID, + groupType: GroupType.AVChatRoom, + groupName: groupName, + ) + .timeout( + _createRoomGroupTimeout, + onTimeout: + () => V2TimValueCallback( + code: -10001, + desc: 'Create IM group timeout', + ), + ); } ///加入房间的im群聊 @@ -1931,28 +1941,6 @@ class RealTimeMessagingManager extends ChangeNotifier { currentContext, listen: false, ); - final isCurrentVisibleRoom = - rtcProvider.shouldShowRoomVisualEffects && - rtcProvider.isVoiceRoomRouteVisible; - final launch = _buildRoomRocketLaunchFromStatusPayload( - rtcProvider: rtcProvider, - payload: data, - roomId: roomId, - ); - final shouldHandleLaunch = _shouldHandleRoomRocketLaunchFromStatusPayload( - rtcProvider: rtcProvider, - payload: data, - launch: launch, - ); - if (shouldHandleLaunch) { - _publishRoomRocketRegionBroadcastIfNeeded(launch); - if (isCurrentVisibleRoom) { - _handleCurrentVisibleRoomRocketLaunch( - rtcProvider: rtcProvider, - launch: launch, - ); - } - } unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId)); } @@ -2330,141 +2318,6 @@ class RealTimeMessagingManager extends ChangeNotifier { return ['rocket_launch_loose', launch.roomId, launch.safeLevel].join('|'); } - bool _shouldHandleRoomRocketLaunchFromStatusPayload({ - required RealTimeCommunicationManager rtcProvider, - required Map payload, - required RoomRocketLaunchBroadcastMessage launch, - }) { - final currentPercent = _roomRocketPayloadPercent(payload); - if (currentPercent < 99.5) { - return false; - } - final previousPercent = _roomRocketStatusPercent( - rtcProvider.roomRocketStatus, - ); - if (previousPercent < 99.5) { - return true; - } - final launchKey = _roomRocketLaunchNoticeKey(launch); - if (launchKey.isEmpty) { - return false; - } - _pruneRecentRoomRocketLaunches(DateTime.now().millisecondsSinceEpoch); - final looseLaunchKey = _roomRocketLaunchLooseKey(launch); - return !_recentRoomRocketLaunchTimes.containsKey(launchKey) && - (looseLaunchKey.isEmpty || - !_recentRoomRocketLaunchLooseTimes.containsKey(looseLaunchKey)); - } - - RoomRocketLaunchBroadcastMessage _buildRoomRocketLaunchFromStatusPayload({ - required RealTimeCommunicationManager rtcProvider, - required Map payload, - required String roomId, - }) { - final roomProfile = rtcProvider.currenRoom?.roomProfile?.roomProfile; - final level = _roomRocketPayloadLevel( - payload, - rtcProvider.roomRocketStatus?.currentLevel ?? - rtcProvider.roomRocketStatus?.level?.toInt() ?? - 1, - ); - return RoomRocketLaunchBroadcastMessage( - roomId: roomId, - roomAccount: _firstNonBlank([ - roomProfile?.roomAccount, - _payloadText(payload['roomAccount'] ?? payload['room_account']), - ]), - roomName: _firstNonBlank([ - roomProfile?.roomName, - _payloadText(payload['roomName'] ?? payload['room_name']), - ]), - roomCoverUrl: _firstNonBlank([ - roomProfile?.roomCover, - _payloadAssetText(payload['roomCoverUrl'] ?? payload['room_cover_url']), - _payloadAssetText(payload['roomCover'] ?? payload['room_cover']), - _payloadAssetText(payload['roomAvatar'] ?? payload['room_avatar']), - _payloadAssetText(payload['roomIcon'] ?? payload['room_icon']), - _payloadAssetText(payload['coverUrl'] ?? payload['cover_url']), - _payloadAssetText(payload['cover']), - _payloadAssetText(_broadcastPayloadMap(payload['room'])['coverUrl']), - _payloadAssetText(_broadcastPayloadMap(payload['room'])['cover']), - _payloadAssetText(_broadcastPayloadMap(payload['room'])['avatar']), - _payloadAssetText( - _broadcastPayloadMap(payload['roomInfo'])['coverUrl'], - ), - _payloadAssetText(_broadcastPayloadMap(payload['roomInfo'])['cover']), - _payloadAssetText(_broadcastPayloadMap(payload['roomInfo'])['avatar']), - ]), - roundNo: - _payloadNum(payload['roundNo'] ?? payload['round_no'])?.toInt() ?? 0, - level: level, - rocketIconUrl: _firstNonBlank([ - _rocketIconFromPayload(payload), - _rocketIconUrlForLevel(level), - ]), - rocketAnimationUrl: _rocketAnimationFromPayload(payload), - durationSeconds: _roomRocketPayloadNoticeSeconds(payload), - triggerUser: const RoomRocketLaunchTriggerUser( - userId: '', - nickname: '', - avatar: '', - account: '', - countryCode: '', - countryName: '', - ), - triggerUserId: _payloadText( - payload['triggerUserId'] ?? payload['trigger_user_id'], - ), - launchNo: _payloadText(payload['launchNo'] ?? payload['launch_no']), - ); - } - - 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['display_percent'] ?? - payload['energyPercent'] ?? - payload['energy_percent'] ?? - 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'] ?? payload['current_energy'], - ); - final maxEnergy = _payloadNum( - payload['maxEnergy'] ?? - payload['max_energy'] ?? - payload['needEnergy'] ?? - payload['need_energy'], - ); - 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'] ?? @@ -2478,25 +2331,6 @@ class RealTimeMessagingManager extends ChangeNotifier { return (value?.toInt() ?? fallback).clamp(1, 99).toInt(); } - int _roomRocketPayloadNoticeSeconds(Map payload) { - final seconds = - _payloadNum( - payload['countdownSeconds'] ?? - payload['countdown_seconds'] ?? - payload['launchDelaySeconds'] ?? - payload['launch_delay_seconds'] ?? - payload['delaySeconds'] ?? - payload['delay_seconds'] ?? - payload['duration_seconds'] ?? - payload['durationSeconds'], - )?.toInt() ?? - 10; - if (seconds > 0 && seconds <= 60) { - return seconds; - } - return 10; - } - String _rocketIconFromPayload(Map payload) { return _firstNonBlank([ _payloadAssetText(payload['rocketIconUrl']), @@ -2508,22 +2342,6 @@ class RealTimeMessagingManager extends ChangeNotifier { ]); } - String _rocketAnimationFromPayload(Map payload) { - return _firstNonBlank([ - _payloadAssetText(payload['rocketAnimationUrl']), - _payloadAssetText(payload['rocket_animation_url']), - _payloadAssetText(payload['rocketSvgaUrl']), - _payloadAssetText(payload['rocket_svga_url']), - _payloadAssetText(payload['animationUrl']), - _payloadAssetText(payload['animation_url']), - _payloadAssetText(payload['launchAnimationUrl']), - _payloadAssetText(payload['launch_animation_url']), - _payloadAssetText(payload['svgaUrl']), - _payloadAssetText(payload['sourceUrl']), - _payloadAssetText(payload['source_url']), - ]); - } - String _payloadRoomId(Map payload) { return _firstNonBlank([ _payloadText(payload['roomId']), diff --git a/lib/services/audio/trtc_room_rtc_engine_adapter.dart b/lib/services/audio/trtc_room_rtc_engine_adapter.dart index 765d2ae..e1dc05c 100644 --- a/lib/services/audio/trtc_room_rtc_engine_adapter.dart +++ b/lib/services/audio/trtc_room_rtc_engine_adapter.dart @@ -206,6 +206,9 @@ class TrtcRoomRtcEngineAdapter implements RoomRtcEngineAdapter { @override RoomRtcEngineKind get kind => RoomRtcEngineKind.trtc; + @override + bool get isInRoom => _isEntered; + @override Future prewarm() async {} diff --git a/lib/services/room/rc_room_manager.dart b/lib/services/room/rc_room_manager.dart index c6f40a5..a028dd0 100644 --- a/lib/services/room/rc_room_manager.dart +++ b/lib/services/room/rc_room_manager.dart @@ -78,22 +78,28 @@ class SocialChatRoomManager extends ChangeNotifier { void createNewRoom(BuildContext context, {String? customName}) async { SCLoadingManager.show(context: context); - // 差异化:添加自定义名称参数(暂未使用,为未来扩展预留) - if (customName != null) { - // TODO: 实现自定义房间名称 - } - await SCAccountRepository().createRoom(); - myRoom = await SCAccountRepository().myProfile(); - hasLoadedMyRoom = true; - isMyRoomLoading = false; - if (!context.mounted) { + try { + // 差异化:添加自定义名称参数(暂未使用,为未来扩展预留) + if (customName != null) { + // TODO: 实现自定义房间名称 + } + await SCAccountRepository().createRoom(); + myRoom = await SCAccountRepository().myProfile(); + hasLoadedMyRoom = true; + isMyRoomLoading = false; + if (!context.mounted) { + return; + } + SCTts.show(SCAppLocalizations.of(context)!.createRoomSuccsess); + } catch (e) { + isMyRoomLoading = false; + if (context.mounted) { + SCTts.show(e.toString()); + } + } finally { notifyListeners(); SCLoadingManager.hide(); - return; } - SCTts.show(SCAppLocalizations.of(context)!.createRoomSuccsess); - notifyListeners(); - SCLoadingManager.hide(); } void clearContributionLevelData({bool notify = true}) { diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index 8be2346..6be89f2 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -848,6 +848,7 @@ class WearBadge { String? milestone, String? notSelectUrl, String? selectUrl, + String? sourceType, String? type, String? userId, bool? use, @@ -862,6 +863,7 @@ class WearBadge { _milestone = milestone; _notSelectUrl = notSelectUrl; _selectUrl = selectUrl; + _sourceType = sourceType; _type = type; _userId = userId; } @@ -882,7 +884,10 @@ class WearBadge { _milestone = _stringValue(json['milestone']); _notSelectUrl = _stringValue(json['notSelectUrl']); _selectUrl = _stringValue(json['url'] ?? json['selectUrl']); - _type = _stringValue(json['type']); + final rawType = _stringValue(json['type']); + _sourceType = + _stringValue(json['sourceType']) ?? _sourceTypeFromType(rawType); + _type = rawType; _userId = _stringValue(json['userId']); } @@ -896,6 +901,7 @@ class WearBadge { String? _milestone; String? _notSelectUrl; String? _selectUrl; + String? _sourceType; String? _type; String? _userId; @@ -910,6 +916,7 @@ class WearBadge { String? milestone, String? notSelectUrl, String? selectUrl, + String? sourceType, String? type, String? userId, }) => WearBadge( @@ -923,6 +930,7 @@ class WearBadge { milestone: milestone ?? _milestone, notSelectUrl: notSelectUrl ?? _notSelectUrl, selectUrl: selectUrl ?? _selectUrl, + sourceType: sourceType ?? _sourceType, type: type ?? _type, userId: userId ?? _userId, ); @@ -947,6 +955,8 @@ class WearBadge { String? get selectUrl => _selectUrl; + String? get sourceType => _sourceType; + String? get type => _type; String? get userId => _userId; @@ -963,6 +973,7 @@ class WearBadge { map['milestone'] = _milestone; map['notSelectUrl'] = _notSelectUrl; map['selectUrl'] = _selectUrl; + map['sourceType'] = _sourceType; map['type'] = _type; map['userId'] = _userId; return map; @@ -973,6 +984,16 @@ class WearBadge { return text == null || text.isEmpty ? null : text; } + static String? _sourceTypeFromType(String? type) { + final normalized = type?.trim().toUpperCase(); + if (normalized == "VIP" || + normalized == "ACTIVITY" || + normalized == "ACHIEVEMENT") { + return normalized; + } + return null; + } + static int? _intValue(dynamic value) { if (value is int) { return value; diff --git a/lib/shared/business_logic/models/res/sc_user_badge_res.dart b/lib/shared/business_logic/models/res/sc_user_badge_res.dart index 3e6bfea..904fe23 100644 --- a/lib/shared/business_logic/models/res/sc_user_badge_res.dart +++ b/lib/shared/business_logic/models/res/sc_user_badge_res.dart @@ -71,7 +71,7 @@ class SCUserBadgeRes { static bool _isDisplayBadgePayload(Map item) { final type = _stringValue(item['type'])?.toUpperCase(); - if (type != null && type != "LONG" && type != "SHORT") { + if (type != null && !_isDisplayType(type)) { return false; } @@ -84,6 +84,14 @@ class SCUserBadgeRes { sourceType == "ACHIEVEMENT"; } + static bool _isDisplayType(String type) { + return type == "LONG" || + type == "SHORT" || + type == "VIP" || + type == "ACTIVITY" || + type == "ACHIEVEMENT"; + } + static String? _stringValue(dynamic value) { final text = value?.toString().trim(); return text == null || text.isEmpty ? null : text; 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 27ef037..636c2ba 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 @@ -1174,10 +1174,10 @@ class SCAccountRepository implements SocialChatUserRepository { return result; } - ///app/user/badge/current + ///go/app/user/badge/current @override Future currentUserBadges() async { - const path = "/app/user/badge/current"; + const path = "/go/app/user/badge/current"; final result = await http.get( path, extra: const {BaseNetworkClient.silentErrorToastKey: true}, @@ -1186,11 +1186,11 @@ class SCAccountRepository implements SocialChatUserRepository { return result; } - ///app/user/badge/other + ///go/app/user/badge/other @override Future otherUserBadges(String userId) async { final params = {"userId": userId}; - const path = "/app/user/badge/other"; + const path = "/go/app/user/badge/other"; final result = await http.get( path, queryParams: params, diff --git a/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart b/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart index c1578bd..bee2363 100644 --- a/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart +++ b/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart @@ -37,7 +37,8 @@ class SCUserBadgeStrip extends StatefulWidget { } class _SCUserBadgeStripState extends State { - static final Map> _badgeFutures = {}; + static const Duration _cacheTtl = Duration(minutes: 1); + static final Map _badgeFutures = {}; Future? _future; @@ -64,7 +65,16 @@ class _SCUserBadgeStripState extends State { AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; final isCurrentUser = currentUserId.isNotEmpty && userId == currentUserId; final cacheKey = isCurrentUser ? "current:$userId" : "other:$userId"; - return _badgeFutures.putIfAbsent(cacheKey, () async { + final now = DateTime.now(); + _badgeFutures.removeWhere( + (_, entry) => now.difference(entry.createdAt) >= _cacheTtl, + ); + final cacheEntry = _badgeFutures[cacheKey]; + if (cacheEntry != null && + now.difference(cacheEntry.createdAt) < _cacheTtl) { + return cacheEntry.future; + } + final future = () async { try { return isCurrentUser ? await SCAccountRepository().currentUserBadges() @@ -73,7 +83,9 @@ class _SCUserBadgeStripState extends State { _badgeFutures.remove(cacheKey); return SCUserBadgeRes.empty(userId: userId); } - }); + }(); + _badgeFutures[cacheKey] = _BadgeFutureCacheEntry(future, now); + return future; } String get _normalizedUserId => widget.userId?.trim() ?? ""; @@ -146,7 +158,8 @@ class _SCUserBadgeStripState extends State { Widget _buildBadge(WearBadge badge, {required double badgeHeight}) { final type = badge.type?.trim().toUpperCase() ?? ""; - final isLongBadge = type == "LONG"; + final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; + final isLongBadge = type == "LONG" || type == "VIP" || sourceType == "VIP"; final width = isLongBadge ? (widget.longBadgeWidth ?? badgeHeight * 2.2) @@ -180,3 +193,10 @@ class _SCUserBadgeStripState extends State { return spaced; } } + +class _BadgeFutureCacheEntry { + const _BadgeFutureCacheEntry(this.future, this.createdAt); + + final Future future; + final DateTime createdAt; +} diff --git a/lib/ui_kit/widgets/room/room_msg_item.dart b/lib/ui_kit/widgets/room/room_msg_item.dart index aae2a11..f321286 100644 --- a/lib/ui_kit/widgets/room/room_msg_item.dart +++ b/lib/ui_kit/widgets/room/room_msg_item.dart @@ -1242,23 +1242,66 @@ class _MsgItemState extends State { ); } + Widget _buildUserNameAndLevels(SocialChatUserProfile? user) { + return SizedBox( + height: 23.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + fit: FlexFit.loose, + child: socialchatNickNameText( + user?.userNickname ?? "", + maxWidth: 112.w, + fontWeight: FontWeight.w500, + fontSize: 13.sp, + type: user?.getVIP()?.name ?? "", + needScroll: (user?.userNickname?.characters.length ?? 0) > 10, + ), + ), + SizedBox(width: 4.w), + getWealthLevel( + user?.wealthLevel ?? 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), + SizedBox(width: 2.w), + getUserLevel( + user?.charmLevel ?? 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), + ], + ), + ); + } + + List _activeWearBadges(SocialChatUserProfile? user) { + return user?.wearBadge?.where((item) => item.use ?? false).toList() ?? + const []; + } + Widget _buildMedals({ required String? userId, List fallbackBadges = const [], }) { final hasUserId = (userId ?? "").trim().isNotEmpty; if (!hasUserId && fallbackBadges.isEmpty) { - return Container(); + return SizedBox(height: 16.w); } - return Expanded( + return SizedBox( + width: double.infinity, child: SCUserBadgeStrip( userId: userId, fallbackBadges: fallbackBadges, - height: 25.w, - badgeHeight: 25.w, - longBadgeWidth: 55.w, - spacing: 5.w, + height: 16.w, + badgeHeight: 14.w, + longBadgeWidth: 34.w, + spacing: 3.w, maxBadges: 8, + reserveSpace: true, ), ); } @@ -1800,58 +1843,20 @@ class _MsgItemState extends State { ), SizedBox(width: 3.w), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - getWealthLevel( - widget.msg.user?.wealthLevel ?? 0, - width: 48.w, - height: 23.w, - fontSize: 10.sp, - ), - getUserLevel( - widget.msg.user?.charmLevel ?? 0, - width: 48.w, - height: 23.w, - fontSize: 10.sp, - ), - SizedBox(width: 3.w), - socialchatNickNameText( - maxWidth: 120.w, - fontWeight: FontWeight.w500, - widget.msg.user?.userNickname ?? "", - fontSize: 13.sp, - type: widget.msg.user?.getVIP()?.name ?? "", - needScroll: - (widget.msg.user?.userNickname?.characters.length ?? - 0) > - 10, - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - widget.msg.user?.getVIP() != null - ? netImage( - url: widget.msg.user?.getVIP()?.cover ?? "", - width: 25.w, - height: 25.w, - ) - : Container(), - _buildMedals( - userId: widget.msg.user?.id, - fallbackBadges: - widget.msg.user?.wearBadge - ?.where((item) => item.use ?? false) - .toList() ?? - const [], - ), - ], - ), - ], + child: SizedBox( + height: 48.w, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildUserNameAndLevels(widget.msg.user), + SizedBox(height: 2.w), + _buildMedals( + userId: widget.msg.user?.id, + fallbackBadges: _activeWearBadges(widget.msg.user), + ), + ], + ), ), ), ], @@ -1884,56 +1889,20 @@ class _MsgItemState extends State { ), SizedBox(width: 3.w), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - getWealthLevel( - user.wealthLevel ?? 0, - width: 48.w, - height: 23.w, - fontSize: 10.sp, - ), - getUserLevel( - user.charmLevel ?? 0, - width: 48.w, - height: 23.w, - fontSize: 10.sp, - ), - SizedBox(width: 3.w), - socialchatNickNameText( - maxWidth: 120.w, - fontWeight: FontWeight.w500, - user.userNickname ?? "", - fontSize: 13.sp, - type: user.getVIP()?.name ?? "", - needScroll: - (user.userNickname?.characters.length ?? 0) > 10, - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - user.getVIP() != null - ? netImage( - url: user.getVIP()?.cover ?? "", - width: 25.w, - height: 25.w, - ) - : Container(), - _buildMedals( - userId: user.id, - fallbackBadges: - user.wearBadge - ?.where((item) => item.use ?? false) - .toList() ?? - const [], - ), - ], - ), - ], + child: SizedBox( + height: 48.w, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildUserNameAndLevels(user), + SizedBox(height: 2.w), + _buildMedals( + userId: user.id, + fallbackBadges: _activeWearBadges(user), + ), + ], + ), ), ), ], diff --git a/lib/ui_kit/widgets/room/room_user_info_card.dart b/lib/ui_kit/widgets/room/room_user_info_card.dart index ec5cac4..64bdf23 100644 --- a/lib/ui_kit/widgets/room/room_user_info_card.dart +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -23,6 +23,8 @@ import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart' + as room_card; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; @@ -34,12 +36,12 @@ import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; import '../../../modules/chat/chat_route.dart'; class RoomUserInfoCard extends StatefulWidget { - String? userId; + final String? userId; - RoomUserInfoCard({super.key, this.userId}); + const RoomUserInfoCard({super.key, this.userId}); @override - _RoomUserInfoCardState createState() => _RoomUserInfoCardState(); + State createState() => _RoomUserInfoCardState(); } class _RoomUserInfoCardState extends State { @@ -180,7 +182,11 @@ class _RoomUserInfoCardState extends State { SizedBox(height: 6.w), _buildInfoChips(context, profile), SizedBox(height: 9.w), - _buildBadgeStrip(profile?.id ?? widget.userId), + _buildBadgeStrip( + profile?.id ?? widget.userId, + ref, + profile, + ), SizedBox(height: 10.w), _buildActions( context: context, @@ -408,9 +414,14 @@ class _RoomUserInfoCardState extends State { ); } - Widget _buildBadgeStrip(String? userId) { + Widget _buildBadgeStrip( + String? userId, + SocialChatUserProfileManager ref, + SocialChatUserProfile? profile, + ) { return SCUserBadgeStrip( userId: userId, + fallbackBadges: _roomCardFallbackBadges(ref, profile), height: 55.w, badgeHeight: 23.w, longBadgeWidth: 58.w, @@ -421,6 +432,46 @@ class _RoomUserInfoCardState extends State { ); } + List _roomCardFallbackBadges( + SocialChatUserProfileManager ref, + SocialChatUserProfile? profile, + ) { + final wearBadges = + profile?.wearBadge + ?.where((item) => item.use != false && _hasBadgeUrl(item)) + .toList() ?? + const []; + if (wearBadges.isNotEmpty) { + return wearBadges; + } + return ref.userCardInfo?.useBadge + ?.map(_roomUseBadgeToWearBadge) + .where(_hasBadgeUrl) + .toList() ?? + const []; + } + + WearBadge _roomUseBadgeToWearBadge(room_card.UseBadge badge) { + return WearBadge( + animationUrl: badge.animationUrl, + badgeKey: badge.badgeKey, + badgeLevel: badge.badgeLevel, + badgeName: badge.badgeName, + expireTime: badge.expireTime?.toInt(), + id: badge.id, + milestone: badge.milestone, + notSelectUrl: badge.notSelectUrl, + selectUrl: badge.selectUrl, + type: badge.type, + userId: badge.userId, + use: true, + ); + } + + bool _hasBadgeUrl(WearBadge badge) { + return (badge.selectUrl ?? "").trim().isNotEmpty; + } + Widget _buildActions({ required BuildContext context, required SocialChatUserProfileManager ref,