diff --git a/docs/voice-room-emoji-plan.md b/docs/voice-room-emoji-plan.md new file mode 100644 index 0000000..4122409 --- /dev/null +++ b/docs/voice-room-emoji-plan.md @@ -0,0 +1,335 @@ +# Voice Room Emoji Package Plan + +## Goal + +This document only records a feasible implementation plan. No business code is changed in this round. + +Requirement summary: + +- Add an emoji package button next to the voice-room chat entry. +- The emoji package button and text chat must open the same bottom composer sheet. +- Emoji packages are shown only on mic seats. +- Emoji packages must not appear in room chat messages, including the `All`, `Chat`, and `Gift` tabs. +- Wait for final backend data and UI assets before starting the real implementation. + +## Current Codebase Status + +The current project already has several reusable pieces, so the feature is feasible and does not need a brand-new architecture. + +Reusable entry points: + +- Bottom area layout: `lib/ui_kit/widgets/room/room_bottom_widget.dart` +- Chat composer popup: `lib/ui_kit/widgets/room/room_msg_input.dart` +- Seat emoji display: `lib/modules/room/seat/sc_seat_item.dart` +- Room RTM send/receive: `lib/services/audio/rtm_manager.dart` +- Seat transient state: `lib/services/audio/rtc_manager.dart` +- Seat model: `lib/shared/business_logic/models/res/mic_res.dart` +- Emoji material API: `lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart` +- Emoji material model: `lib/shared/business_logic/models/res/sc_room_emoji_res.dart` +- Existing room message type: `lib/app/constants/sc_room_msg_type.dart` + +Confirmed reusable behavior in current code: + +- `SCRoomMsgType.emoticons` already exists. +- The room receiver already handles `EMOTICONS` separately and calls `RtcProvider.starPlayEmoji(msg)`. +- `SCSeatItem` already has an `Emoticons` widget with queueing, fade animation, and auto-dismiss. +- The project already has `/material/emoji/all`, and the response model already supports category + emoji list. + +## Recommended Architecture + +Recommended direction: keep one composer sheet and support two entry modes. + +Recommended entry modes: + +- `text` mode: opened from the existing chat entry, keyboard focused by default. +- `emojiPackage` mode: opened from the new emoji package button, keyboard hidden by default, emoji package panel open by default. + +Recommended widget strategy: + +- Reuse `RoomMsgInput` instead of creating a second bottom sheet. +- Add an input parameter such as `initialMode` or `initialPanel`. +- Keep the text input area as the same top row. +- Use the lower content area to switch between text helper emoji and room emoji package content. + +Why this is the best fit: + +- It matches the requirement that text chat and emoji package use the same bottom popup. +- It keeps interaction and style in one place. +- It avoids future drift between two different room composer implementations. + +## Recommended Interaction Flow + +### 1. Bottom bar + +Add a new emoji package button next to the room chat entry in `RoomBottomWidget`. + +Recommended behavior: + +- Tap chat entry: open `RoomMsgInput(initialMode: text)`. +- Tap emoji package button: open `RoomMsgInput(initialMode: emojiPackage)`. +- If the current user is not on mic, the emoji package button can either be disabled or show a toast such as "Take a mic first". + +Important layout note: + +- `RoomBottomWidget` currently uses hard-coded width math for the bottom bar and for `_resolveGiftCenterX(...)`. +- After adding one more action button, the row layout and gift floating button center calculation both need to be updated together. +- If only the row is changed and `_resolveGiftCenterX(...)` is not updated, the floating gift button can become visually misaligned. + +### 2. Composer sheet + +Recommended sheet structure: + +- Top area: existing input field, send button, and keyboard/emoji switch logic. +- Bottom area in `text` mode: current text emoji helper can remain for typing unicode emoji. +- Bottom area in `emojiPackage` mode: category tabs + emoji package grid from backend data. + +Recommended send interaction for emoji package: + +- Tapping an emoji package item sends immediately. +- The sheet should stay open after sending so the user can send multiple emoji packages continuously. +- If UI later wants tap-to-close, that can be a product choice, but continuous sending is the safer default for room interaction. + +### 3. Seat-only display + +Emoji package messages should not enter room chat lists. + +Recommended rule: + +- Text messages still follow the existing room chat path. +- Emoji package clicks trigger seat animation only. +- No new item should be inserted into `roomAllMsgList`, `roomChatMsgList`, or `roomGiftMsgList`. + +## Recommended Send Path + +### Recommended payload shape + +The smallest-change plan is to reuse the existing room custom message structure: + +```dart +Msg( + groupId: roomAccount, + type: SCRoomMsgType.emoticons, + msg: emoji.sourceUrl, + number: currentSeatIndex, + user: currentUserProfile, + role: currentUserRole, +) +``` + +Recommended field meaning: + +- `type`: `EMOTICONS` +- `msg`: selected emoji resource URL +- `number`: current seat index +- `user`: sender profile, for consistency with existing room messages + +### Why not send it as a normal chat message + +Because `RtmProvider.addMsg(...)` always inserts the message into `roomAllMsgList` first. + +Current receive side is already good enough: + +- In `_newGroupMsg(...)`, `EMOTICONS` goes directly to `RtcProvider.starPlayEmoji(msg)` and then `return`. +- That means remote users will not see emoji package content in room chat. + +Current sender side still needs care during implementation: + +- If the sender uses `dispatchMessage(...)` with default `addLocal: true`, the local sender will still push one item into `roomAllMsgList`. +- That would violate the requirement that emoji packages must not show in chat. + +Recommended implementation behavior later: + +- Build the `Msg`. +- First do a local optimistic seat display by calling `RtcProvider.starPlayEmoji(msg)`. +- Then call `dispatchMessage(msg, addLocal: false)`. + +This gives the sender instant seat feedback while keeping the chat list clean. + +## Recommended Seat Resolution Rule + +When sending an emoji package, determine the seat index at send time, not when the sheet is opened. + +Recommended lookup: + +- Use `RtcProvider.userOnMaiInIndex(currentUserId)` when the user taps an emoji item. + +Reason: + +- The user may switch mic seats after opening the popup. +- Using the latest seat index avoids showing the emoji on the wrong seat. + +If the returned seat index is `-1`: + +- Do not send. +- Show a toast telling the user that only users on mic can use emoji packages. + +## Backend Dependency Assessment + +### Material data + +The project already has `/material/emoji/all` and the local model already supports: + +- category name +- category cover +- category-level ownership state +- category-level amount +- emoji list +- emoji cover URL +- emoji source URL + +This means the current backend structure is already close to the requirement shown in the design. + +Backend items to confirm before development: + +- Whether `/material/emoji/all` is the final interface for this feature +- Whether category ordering is controlled by backend +- Whether each category has a tab icon or cover ready for UI +- Whether `coverUrl` is the correct image for the grid cell +- Whether `sourceUrl` is the real image used on the mic seat +- Whether ownership is category-level only or item-level + +If ownership later becomes item-level, the current model may need extension because it currently looks category-oriented. + +### No extra send API is strictly required + +For the minimal-change version, no new HTTP send API is needed. + +Recommended transport: + +- Use the existing room RTM custom message channel. +- Reuse `EMOTICONS` as the message type. + +This keeps the feature aligned with the current room event architecture. + +## Optional Sync Decision + +There are two possible product definitions for emoji package state: + +### Option A: transient RTM-only state + +Behavior: + +- Emoji package is only an instant seat effect. +- Users who join mid-animation do not need to recover the current emoji state. + +Pros: + +- Smallest frontend change +- No need for backend seat-state persistence +- Best fit for the current codebase + +Recommended default: + +- Yes. This should be the first implementation choice unless product explicitly wants state recovery. + +### Option B: recoverable seat state + +Behavior: + +- If a user joins the room late or reconnects, they can still see the currently active emoji package on the seat during its valid duration. + +If product wants this, backend and model changes are needed: + +- mic list response needs to include active emoji state +- active emoji state should include at least resource URL and expiration timing +- frontend model parsing needs to be updated + +Important current gap: + +- `MicRes.fromJson(...)` does not parse `emojiPath` +- `MicRes.toJson()` also does not serialize `emojiPath` + +So if the final backend plan depends on seat polling or room re-entry recovery, this model area will need a small follow-up patch during implementation. + +## Current Code Caveats To Remember + +### 1. `RoomMsgInput` is text-first today + +Current behavior: + +- It always autofocuses the text field. +- It only supports the local text emoji helper based on `SCEmojiDatas.smileys`. + +Meaning for future implementation: + +- It needs a mode parameter so opening from the emoji package button can skip keyboard autofocus and show the package panel first. + +### 2. Seat emoji display currently uses an implicit field contract + +Current behavior in `RtcProvider.starPlayEmoji(msg)`: + +- It writes `emojiPath: msg.msg` +- It writes `type: msg.type` +- It writes `number: msg.msg` + +Current behavior in `SCSeatItem.Emoticons`: + +- For dice and RPS, `number` is treated like a result value +- For other types, `number` is treated like an image URL + +Meaning: + +- The current image emoji path works because the code uses a field-reuse convention. +- Later implementation should stay compatible with this path unless the seat overlay model is cleaned up in one pass. + +### 3. Emoji cache utility exists but is not wired into the room sheet yet + +`SCAppGeneralManager` already has: + +- `emojiByTab` +- `emojiAll()` + +But this is not currently connected to the room composer. + +Recommended later choice: + +- Either reuse `SCAppGeneralManager` for caching +- Or load emoji package data directly inside the room sheet and cache locally there + +Either is feasible. Reusing the manager is better if the same materials are used in multiple room surfaces. + +## Suggested Development Sequence Later + +When backend and UI are ready, the development order should be: + +1. Add the new emoji package button in `RoomBottomWidget` and adjust layout math together with `_resolveGiftCenterX(...)`. +2. Refactor `RoomMsgInput` into one shared room composer with `text` and `emojiPackage` entry modes. +3. Wire the sheet to load emoji categories and emoji items from `/material/emoji/all`. +4. Add on-mic gating and resolve seat index at click time. +5. On emoji package click, do local optimistic seat playback. +6. Send RTM `EMOTICONS` with `addLocal: false`. +7. Verify that `All`, `Chat`, and `Gift` tabs remain unchanged after sending. +8. If product wants reconnect recovery, then patch `MicRes` parsing and seat sync logic. + +## Acceptance Checklist + +- New emoji package button appears next to the room chat entry. +- Chat entry and emoji package button open the same bottom composer sheet. +- Opening from chat goes to text mode. +- Opening from emoji package goes to emoji package mode. +- Only users on mic can send emoji packages. +- Sender sees the emoji package on their own seat immediately. +- Other users see the emoji package on the sender's seat. +- Emoji package sending does not create new rows in `All`, `Chat`, or `Gift`. +- Existing text chat sending is not affected. +- Existing gift floating button remains visually centered after the new button is added. +- Repeated emoji package taps queue and play correctly on the seat. + +## Final Recommendation + +This feature is feasible with low-to-medium implementation risk because the project already has the key building blocks: + +- shared room composer entry +- room RTM custom message channel +- `EMOTICONS` message type +- seat emoji playback widget +- emoji material API + +Recommended final direction: + +- Reuse the existing room composer popup +- Reuse `EMOTICONS` as the transport type +- Use seat-index-based RTM payload +- Do local optimistic seat playback +- Do not insert emoji package messages into any chat list +- Treat backend seat-state recovery as optional, not mandatory diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 420d4b1..223ca63 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -502,7 +502,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = S9X2AJ2US9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -693,7 +693,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -722,7 +722,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/lib/main.dart b/lib/main.dart index 142b1c6..77a977a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -410,7 +410,8 @@ class _YumiApplicationState extends State { if (SCGlobalConfig.allowsHighCostAnimations) Consumer( builder: (context, rtcProvider, _) { - if (!rtcProvider.roomVisualEffectsEnabled) { + if (!rtcProvider + .shouldShowRoomVisualEffects) { return const SizedBox.shrink(); } return const Positioned.fill( @@ -420,15 +421,24 @@ class _YumiApplicationState extends State { ); }, ), - Positioned.fill( - child: RoomGiftSeatFlightOverlay( - controller: RoomGiftSeatFlightController(), - resolveTargetKey: - (userId) => Provider.of( - context, - listen: false, - ).getSeatGlobalKeyByIndex(userId), - ), + Consumer( + builder: (context, rtcProvider, _) { + if (!rtcProvider + .shouldShowRoomVisualEffects) { + return const SizedBox.shrink(); + } + return Positioned.fill( + child: RoomGiftSeatFlightOverlay( + controller: + RoomGiftSeatFlightController(), + resolveTargetKey: + (userId) => Provider.of( + context, + listen: false, + ).getSeatGlobalKeyByIndex(userId), + ), + ); + }, ), ], ); diff --git a/lib/modules/room/detail/room_detail_page.dart b/lib/modules/room/detail/room_detail_page.dart index 6d2b505..b0e4c3a 100644 --- a/lib/modules/room/detail/room_detail_page.dart +++ b/lib/modules/room/detail/room_detail_page.dart @@ -21,6 +21,7 @@ import '../../../ui_kit/components/sc_debounce_widget.dart'; import '../../../ui_kit/components/sc_tts.dart'; import '../../../ui_kit/components/dialog/dialog_base.dart'; import '../../../ui_kit/components/text/sc_text.dart'; +import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; import '../../../main.dart'; import '../../index/main_route.dart'; @@ -153,10 +154,41 @@ class _RoomDetailPageState extends State { SizedBox(height: 5.w), Row( children: [ - text( - "ID:${ref.currenRoom?.roomProfile?.userProfile?.getID()}", - fontSize: 12.sp, - textColor: Colors.black, + SCSpecialIdBadge( + idText: + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomAccount ?? + "", + showAnimated: + (ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomAccount + ?.isNotEmpty ?? + false), + assetPath: + SCSpecialIdAssets.roomId, + animationWidth: 124.w, + animationHeight: 30.w, + textPadding: EdgeInsets.fromLTRB( + 36.w, + 7.w, + 18.w, + 7.w, + ), + animationTextStyle: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w700, + ), + normalTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + ), ), SizedBox(width: 3.w), GestureDetector( @@ -278,10 +310,44 @@ class _RoomDetailPageState extends State { ), Row( children: [ - text( - "ID:${ref.currenRoom?.roomProfile?.userProfile?.account}", - textColor: Colors.black, - fontSize: 14.sp, + SCSpecialIdBadge( + idText: + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.getID() ?? + "", + showAnimated: + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.hasSpecialId() ?? + false, + assetPath: + SCSpecialIdAssets + .roomOwnerId, + animationWidth: 132.w, + animationHeight: 32.w, + textPadding: + EdgeInsets.fromLTRB( + 40.w, + 8.w, + 18.w, + 8.w, + ), + animationTextStyle: + TextStyle( + color: Colors.white, + fontSize: 13.sp, + fontWeight: + FontWeight.w700, + ), + normalTextStyle: TextStyle( + color: Colors.black, + fontSize: 14.sp, + ), ), SizedBox(width: 3.w), GestureDetector( @@ -304,7 +370,7 @@ class _RoomDetailPageState extends State { .currenRoom ?.roomProfile ?.userProfile - ?.account ?? + ?.getID() ?? "", ), ); diff --git a/lib/modules/room/online/room_online_page.dart b/lib/modules/room/online/room_online_page.dart index 44476e5..4372144 100644 --- a/lib/modules/room/online/room_online_page.dart +++ b/lib/modules/room/online/room_online_page.dart @@ -12,6 +12,7 @@ import '../../../ui_kit/components/sc_compontent.dart'; import '../../../ui_kit/components/sc_page_list.dart'; import '../../../ui_kit/components/sc_tts.dart'; import '../../../ui_kit/components/text/sc_text.dart'; +import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; import '../../../shared/tools/sc_lk_dialog_util.dart'; import '../../../ui_kit/widgets/room/room_user_info_card.dart'; @@ -148,22 +149,26 @@ class _RoomOnlinePageState GestureDetector( child: Container( padding: EdgeInsets.symmetric(vertical: 3.w), - child: Row( - textDirection: TextDirection.ltr, - children: [ - text( - "ID:${userInfo.getID()}", - fontSize: 12.sp, - textColor: Colors.black, - fontWeight: FontWeight.bold, - ), - SizedBox(width: 5.w), - Image.asset( - "sc_images/room/sc_icon_user_card_copy_id.png", - width: 12.w, - height: 12.w, - ), - ], + child: SCSpecialIdBadge( + idText: userInfo.getID(), + showAnimated: userInfo.hasSpecialId(), + assetPath: SCSpecialIdAssets.userId, + animationWidth: 110.w, + animationHeight: 28.w, + textPadding: EdgeInsets.fromLTRB(33.w, 6.w, 16.w, 6.w), + animationTextStyle: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + normalTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + showCopyIcon: true, + copyIconSize: 12.w, + copyIconSpacing: 5.w, ), ), onTap: () { diff --git a/lib/modules/room/seat/sc_seat_item.dart b/lib/modules/room/seat/sc_seat_item.dart index 0ee35b1..9c1592e 100644 --- a/lib/modules/room/seat/sc_seat_item.dart +++ b/lib/modules/room/seat/sc_seat_item.dart @@ -28,6 +28,7 @@ class SCSeatItem extends StatefulWidget { class _SCSeatItemState extends State with TickerProviderStateMixin { static const String _seatHeartbeatValueIconAsset = "sc_images/room/sc_icon_room_seat_heartbeat_value.png"; + static const double _seatHeaddressScaleMultiplier = 1.3; RtcProvider? provider; _SeatRenderSnapshot? _cachedSnapshot; @@ -56,6 +57,8 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { window.locale.languageCode == "ar" ? "" : seatSnapshot.headdressSourceUrl; + final double seatBaseSize = widget.isGameModel ? 40.w : 55.w; + final bool hasHeaddress = resolvedHeaddress.isNotEmpty; return GestureDetector( behavior: HitTestBehavior.opaque, @@ -66,9 +69,10 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { children: [ SizedBox( key: _targetKey, - width: widget.isGameModel ? 40.w : 55.w, - height: widget.isGameModel ? 40.w : 55.w, + width: seatBaseSize, + height: seatBaseSize, child: Stack( + clipBehavior: Clip.none, alignment: Alignment.center, children: [ Sonic( @@ -77,10 +81,15 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { isGameModel: widget.isGameModel, ), seatSnapshot.hasUser - ? head( - url: seatSnapshot.userAvatar, - width: widget.isGameModel ? 40.w : 55.w, - headdress: resolvedHeaddress, + ? Transform.scale( + scale: + hasHeaddress ? _seatHeaddressScaleMultiplier : 1, + child: head( + url: seatSnapshot.userAvatar, + width: seatBaseSize, + height: seatBaseSize, + headdress: resolvedHeaddress, + ), ) : (seatSnapshot.micLock ? Image.asset( @@ -231,7 +240,7 @@ class _SeatRenderSnapshot { }); factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) { - final roomSeat = provider.roomWheatMap[index]; + final roomSeat = provider.micAtIndexForDisplay(index); final user = roomSeat?.user; return _SeatRenderSnapshot( isExitingCurrentVoiceRoomSession: @@ -398,8 +407,9 @@ class _EmoticonsState extends State with TickerProviderStateMixin { Widget build(BuildContext context) { return Selector( selector: - (context, provider) => - _SeatEmojiSnapshot.fromMic(provider.roomWheatMap[widget.index]), + (context, provider) => _SeatEmojiSnapshot.fromMic( + provider.micAtIndexForDisplay(widget.index), + ), builder: ( BuildContext context, _SeatEmojiSnapshot snapshot, diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index dbb4dc6..5b98fc5 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -14,7 +14,10 @@ import 'package:yumi/services/gift/gift_animation_manager.dart'; import 'package:yumi/services/gift/gift_system_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart'; @@ -138,6 +141,8 @@ class _VoiceRoomPageState extends State RoomEntranceHelper.clearQueue(); _clearLuckyGiftComboSessions(); _giftSeatFlightController.clear(); + OverlayManager().removeRoom(); + SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend'); SCGiftVapSvgaManager().stopPlayback(); } @@ -355,6 +360,21 @@ class _VoiceRoomPageState extends State giftModel.sendUserPic = msg.user?.userAvatar ?? ""; giftModel.giftPic = msg.gift?.giftPhoto ?? ""; giftModel.giftCount = msg.number ?? 0; + giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg); + unawaited( + warmImageResource( + giftModel.sendUserPic, + logicalWidth: 26.w, + logicalHeight: 26.w, + ), + ); + unawaited( + warmImageResource( + giftModel.giftPic, + logicalWidth: 34.w, + logicalHeight: 34.w, + ), + ); Provider.of( context, listen: false, @@ -398,6 +418,20 @@ class _VoiceRoomPageState extends State giftModel.rewardAmount = awardAmount; giftModel.showLuckyRewardFrame = true; giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount); + unawaited( + warmImageResource( + giftModel.sendUserPic, + logicalWidth: 26.w, + logicalHeight: 26.w, + ), + ); + unawaited( + warmImageResource( + giftModel.giftPic, + logicalWidth: 34.w, + logicalHeight: 34.w, + ), + ); Provider.of( context, listen: false, @@ -443,11 +477,13 @@ class _VoiceRoomPageState extends State ); session.endTimer?.cancel(); session.clearQueueTimer?.cancel(); + final previousTotal = session.totalCount; session.totalCount += quantity; final highestMilestone = - SocialChatGiftSystemManager.resolveHighestReachedComboMilestone( - session.totalCount, + SocialChatGiftSystemManager.resolveHighestCrossedComboEffectMilestone( + previousCount: previousTotal, + currentCount: session.totalCount, ); if (highestMilestone != null && highestMilestone > session.highestPlayedMilestone && @@ -527,12 +563,28 @@ class _VoiceRoomPageState extends State return math.max(msg.customAnimationCount ?? 1, 1); } + num _resolveGiftCountStepUnit(Msg msg) { + final quantity = msg.number ?? 0; + final clickCount = math.max(msg.customAnimationCount ?? 1, 1); + if (quantity <= 0) { + return 1; + } + final stepUnit = quantity / clickCount; + if (stepUnit <= 0) { + return quantity; + } + return stepUnit; + } + void _enqueueTrackedSeatFlightAnimations({ required String sessionKey, required String giftPhoto, required String targetUserId, required int animationCount, }) { + unawaited( + warmImageResource(giftPhoto, logicalWidth: 96.w, logicalHeight: 96.w), + ); final normalizedAnimationCount = math.max(animationCount, 1); final cappedAnimationCount = math.min( normalizedAnimationCount, diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart index ed8d544..aeafacc 100644 --- a/lib/modules/user/me_page2.dart +++ b/lib/modules/user/me_page2.dart @@ -14,6 +14,7 @@ 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/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart'; class MePage2 extends StatefulWidget { const MePage2({super.key}); @@ -153,9 +154,19 @@ class _MePage2State extends State { ), ), SizedBox(height: 2.w), - Text( - 'ID:${profile?.account ?? ''}', - style: TextStyle( + SCSpecialIdBadge( + idText: profile?.getID() ?? '', + showAnimated: profile?.hasSpecialId() ?? false, + assetPath: SCSpecialIdAssets.userIdLarge, + animationWidth: 150.w, + animationHeight: 34.w, + textPadding: EdgeInsets.fromLTRB(48.w, 8.w, 18.w, 8.w), + animationTextStyle: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + normalTextStyle: TextStyle( color: Colors.white, fontSize: 16.sp, fontWeight: FontWeight.w600, diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 3f5c631..882163a 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -34,6 +34,7 @@ import '../../../app/constants/sc_screen.dart'; import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; import '../../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; +import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; import '../../chat/chat_route.dart'; class PersonDetailPage extends StatefulWidget { @@ -398,11 +399,23 @@ class _PersonDetailPageState extends State Row( children: [ SizedBox(width: 25.w), - text( - "ID:${ref.userProfile?.account ?? ""}", - textColor: Colors.white, - fontWeight: FontWeight.w400, - fontSize: 16.sp, + SCSpecialIdBadge( + idText: ref.userProfile?.getID() ?? "", + showAnimated: ref.userProfile?.hasSpecialId() ?? false, + assetPath: SCSpecialIdAssets.userIdLarge, + animationWidth: 150.w, + animationHeight: 34.w, + textPadding: EdgeInsets.fromLTRB(48.w, 8.w, 18.w, 8.w), + animationTextStyle: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + normalTextStyle: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w400, + ), ), ], ), diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 0b7683d..450224e 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -48,7 +48,7 @@ typedef RtcProvider = RealTimeCommunicationManager; class RealTimeCommunicationManager extends ChangeNotifier { static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); - static const Duration _selfMicSwitchGracePeriod = Duration(seconds: 4); + static const Duration _selfMicStateGracePeriod = Duration(seconds: 4); static const Duration _giftTriggeredMicRefreshMinInterval = Duration( milliseconds: 900, ); @@ -66,6 +66,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { num? _preferredSelfMicIndex; num? _pendingSelfMicSourceIndex; int? _pendingSelfMicSwitchGuardUntilMs; + num? _pendingSelfMicReleaseIndex; + int? _pendingSelfMicReleaseGuardUntilMs; ClientRoleType? _lastAppliedClientRole; bool? _lastAppliedLocalAudioMuted; bool? _lastScheduledVoiceLiveOnMic; @@ -285,10 +287,11 @@ class RealTimeCommunicationManager extends ChangeNotifier { required num sourceIndex, required num targetIndex, }) { + _clearSelfMicReleaseGuard(); _preferredSelfMicIndex = targetIndex; _pendingSelfMicSourceIndex = sourceIndex; _pendingSelfMicSwitchGuardUntilMs = - DateTime.now().add(_selfMicSwitchGracePeriod).millisecondsSinceEpoch; + DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch; } void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) { @@ -299,6 +302,41 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } + void _startSelfMicReleaseGuard({required num sourceIndex}) { + _pendingSelfMicReleaseIndex = sourceIndex; + _pendingSelfMicReleaseGuardUntilMs = + DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch; + } + + void _clearSelfMicReleaseGuard() { + _pendingSelfMicReleaseIndex = null; + _pendingSelfMicReleaseGuardUntilMs = null; + } + + bool _shouldSuppressCurrentUserOnSeat(num index, {MicRes? seat}) { + final currentUserId = + (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); + if (currentUserId.isEmpty) { + return false; + } + final releaseIndex = _pendingSelfMicReleaseIndex; + final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0; + if (releaseIndex != index || + releaseGuardUntilMs <= DateTime.now().millisecondsSinceEpoch) { + return false; + } + final resolvedSeat = seat ?? roomWheatMap[index]; + return (resolvedSeat?.user?.id ?? "").trim() == currentUserId; + } + + MicRes? micAtIndexForDisplay(num index) { + final seat = roomWheatMap[index]; + if (!_shouldSuppressCurrentUserOnSeat(index, seat: seat)) { + return seat; + } + return seat?.copyWith(clearUser: true); + } + Map _stabilizeSelfMicSnapshot( Map nextMap, { required Map previousMap, @@ -308,14 +346,13 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (currentUserId.isEmpty) { return nextMap; } + final nowMs = DateTime.now().millisecondsSinceEpoch; final targetIndex = _preferredSelfMicIndex; final sourceIndex = _pendingSelfMicSourceIndex; final guardUntilMs = _pendingSelfMicSwitchGuardUntilMs ?? 0; final hasActiveGuard = - targetIndex != null && - sourceIndex != null && - guardUntilMs > DateTime.now().millisecondsSinceEpoch; + targetIndex != null && sourceIndex != null && guardUntilMs > nowMs; final selfSeatIndices = nextMap.entries .where((entry) => entry.value.user?.id == currentUserId) @@ -330,63 +367,90 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (!hasActiveGuard) { _clearSelfMicSwitchGuard(); - return nextMap; - } - final resolvedTargetIndex = targetIndex; - final resolvedSourceIndex = sourceIndex; + } else { + final resolvedTargetIndex = targetIndex; + final resolvedSourceIndex = sourceIndex; - final selfOnlyOnSource = - selfSeatIndices.isEmpty || - selfSeatIndices.every((seatIndex) => seatIndex == resolvedSourceIndex); - if (!selfOnlyOnSource) { + final selfOnlyOnSource = + selfSeatIndices.isEmpty || + selfSeatIndices.every( + (seatIndex) => seatIndex == resolvedSourceIndex, + ); + if (selfOnlyOnSource) { + final optimisticTargetSeat = previousMap[resolvedTargetIndex]; + if (optimisticTargetSeat != null && + optimisticTargetSeat.user?.id == currentUserId) { + final incomingTargetSeat = nextMap[resolvedTargetIndex]; + if (incomingTargetSeat?.user == null || + incomingTargetSeat?.user?.id == currentUserId) { + final stabilizedMap = Map.from(nextMap); + for (final seatIndex in selfSeatIndices) { + if (seatIndex == resolvedTargetIndex) { + continue; + } + final seat = stabilizedMap[seatIndex]; + if (seat == null) { + continue; + } + stabilizedMap[seatIndex] = seat.copyWith(clearUser: true); + } + + final baseSeat = incomingTargetSeat ?? optimisticTargetSeat; + stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith( + user: optimisticTargetSeat.user, + micMute: + incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute, + micLock: + incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock, + roomToken: + incomingTargetSeat?.roomToken ?? + optimisticTargetSeat.roomToken, + emojiPath: + (incomingTargetSeat?.emojiPath ?? "").isNotEmpty + ? incomingTargetSeat?.emojiPath + : optimisticTargetSeat.emojiPath, + type: + (incomingTargetSeat?.type ?? "").isNotEmpty + ? incomingTargetSeat?.type + : optimisticTargetSeat.type, + number: + (incomingTargetSeat?.number ?? "").isNotEmpty + ? incomingTargetSeat?.number + : optimisticTargetSeat.number, + ); + + return stabilizedMap; + } + } + } + } + + final releaseIndex = _pendingSelfMicReleaseIndex; + final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0; + final hasActiveReleaseGuard = + releaseIndex != null && releaseGuardUntilMs > nowMs; + if (!hasActiveReleaseGuard) { + _clearSelfMicReleaseGuard(); return nextMap; } - final optimisticTargetSeat = previousMap[resolvedTargetIndex]; - if (optimisticTargetSeat == null || - optimisticTargetSeat.user?.id != currentUserId) { + if (selfSeatIndices.isEmpty) { return nextMap; } - final incomingTargetSeat = nextMap[resolvedTargetIndex]; - if (incomingTargetSeat?.user != null && - incomingTargetSeat?.user?.id != currentUserId) { + if (selfSeatIndices.any((seatIndex) => seatIndex != releaseIndex)) { + _clearSelfMicReleaseGuard(); + return nextMap; + } + + final releaseSeat = nextMap[releaseIndex]; + if (releaseSeat?.user?.id != currentUserId) { + _clearSelfMicReleaseGuard(); return nextMap; } final stabilizedMap = Map.from(nextMap); - for (final seatIndex in selfSeatIndices) { - if (seatIndex == resolvedTargetIndex) { - continue; - } - final seat = stabilizedMap[seatIndex]; - if (seat == null) { - continue; - } - stabilizedMap[seatIndex] = seat.copyWith(clearUser: true); - } - - final baseSeat = incomingTargetSeat ?? optimisticTargetSeat; - stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith( - user: optimisticTargetSeat.user, - micMute: incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute, - micLock: incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock, - roomToken: - incomingTargetSeat?.roomToken ?? optimisticTargetSeat.roomToken, - emojiPath: - (incomingTargetSeat?.emojiPath ?? "").isNotEmpty - ? incomingTargetSeat?.emojiPath - : optimisticTargetSeat.emojiPath, - type: - (incomingTargetSeat?.type ?? "").isNotEmpty - ? incomingTargetSeat?.type - : optimisticTargetSeat.type, - number: - (incomingTargetSeat?.number ?? "").isNotEmpty - ? incomingTargetSeat?.number - : optimisticTargetSeat.number, - ); - + stabilizedMap[releaseIndex] = releaseSeat!.copyWith(clearUser: true); return stabilizedMap; } @@ -1208,6 +1272,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { needUpDataUserInfo = false; SCRoomUtils.roomUsersMap.clear(); _clearSelfMicSwitchGuard(clearPreferredIndex: true); + _clearSelfMicReleaseGuard(); roomIsMute = false; rtmProvider?.roomAllMsgList.clear(); rtmProvider?.roomChatMsgList.clear(); @@ -1405,6 +1470,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { final isSeatSwitching = previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex; if (myUser != null) { + _clearSelfMicReleaseGuard(); if (isSeatSwitching) { _startSelfMicSwitchGuard( sourceIndex: previousSelfSeatIndex, @@ -1454,20 +1520,30 @@ class RealTimeCommunicationManager extends ChangeNotifier { index, ); - if (roomWheatMap[index]?.user?.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { + final currentUserId = + AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; + final currentUserSeatIndex = userOnMaiInIndex(currentUserId); + if (roomWheatMap[index]?.user?.id == currentUserId) { isMic = true; engine?.muteLocalAudioStream(true); } _clearSelfMicSwitchGuard(clearPreferredIndex: true); + if (currentUserSeatIndex > -1) { + _startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex); + } else { + _clearSelfMicReleaseGuard(); + } SCHeartbeatUtils.cancelAnchorTimer(); /// 设置成主持人角色 engine?.renewToken(""); engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - _clearUserFromSeats(AccountStorage().getCurrentUser()?.userProfile?.id); + _clearUserFromSeats(currentUserId); notifyListeners(); - _refreshMicListSilently(); + requestMicrophoneListRefresh( + notifyIfUnchanged: false, + minInterval: const Duration(milliseconds: 350), + ); } catch (ex) { SCTts.show('Failed to leave the microphone, $ex'); } @@ -1510,23 +1586,41 @@ class RealTimeCommunicationManager extends ChangeNotifier { ///自己是否在麦上 bool isOnMai() { - return roomWheatMap.values - .map((userWheat) => userWheat.user?.id) - .toList() - .contains(AccountStorage().getCurrentUser()?.userProfile?.id); + final currentUserId = + (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); + if (currentUserId.isEmpty) { + return false; + } + for (final entry in roomWheatMap.entries) { + if ((micAtIndexForDisplay(entry.key)?.user?.id ?? "").trim() == + currentUserId) { + return true; + } + } + return false; } ///自己是否在指定的麦上 bool isOnMaiInIndex(num index) { - return roomWheatMap[index]?.user?.id == - AccountStorage().getCurrentUser()?.userProfile?.id; + return (micAtIndexForDisplay(index)?.user?.id ?? "").trim() == + (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); } ///点击的用户在哪个麦上 num userOnMaiInIndex(String userId) { + final normalizedUserId = userId.trim(); + if (normalizedUserId.isEmpty) { + return -1; + } num index = -1; roomWheatMap.forEach((k, value) { - if (value.user?.id == userId) { + final visibleSeat = + normalizedUserId == + (AccountStorage().getCurrentUser()?.userProfile?.id ?? "") + .trim() + ? micAtIndexForDisplay(k) + : value; + if ((visibleSeat?.user?.id ?? "").trim() == normalizedUserId) { index = k; } }); diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index fdcece2..f6cacd8 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -1,1836 +1,1890 @@ -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; -import 'package:agora_rtc_engine/agora_rtc_engine.dart'; -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_debouncer/flutter_debouncer.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/ui_kit/components/sc_tts.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/tools/sc_message_utils.dart'; -import 'package:yumi/shared/tools/sc_path_utils.dart'; -import 'package:yumi/shared/tools/sc_room_utils.dart'; -import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:provider/provider.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; -import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; -import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart'; -import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; -import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; -import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; -import 'package:yumi/app/constants/sc_room_msg_type.dart'; -import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; -import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; -import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; -import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart'; -import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; -import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart' - hide Data; -import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; -import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart'; -import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; -import 'package:yumi/services/gift/gift_system_manager.dart'; - -import '../../shared/data_sources/models/enum/sc_gift_type.dart'; -import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; - -typedef RoomNewMsgListener = Function(Msg msg); -typedef OnNewMessageListener = Function(V2TimMessage? message, {String? msgId}); -typedef OnNewANMessageListener = Function(Records? message); -typedef OnRevokeMessageListener = Function(String msgId); -typedef OnNewGroupMessageListener = - Function(String groupID, V2TimMessage message); -typedef OnMessageRecvC2CReadListener = Function(List messageIDList); -typedef RtmProvider = RealTimeMessagingManager; - -class RealTimeMessagingManager extends ChangeNotifier { - static const int _giftComboMergeWindowMs = 3000; - static const int _maxLuckGiftPushQueueLength = 12; - static const int _luckyGiftFloatMinMultiple = 5; - static const int _luckyGiftBurstMinMultiple = 10; - static const int _luckyGiftBurstMinAwardAmount = 5000; - - BuildContext? context; - - void _giftFxLog(String message) { - debugPrint('[GiftFX][RTM] $message'); - } - - ///消息列表 - List roomAllMsgList = []; - List roomChatMsgList = []; - List roomGiftMsgList = []; - RoomNewMsgListener? msgAllListener; - RoomNewMsgListener? msgChatListener; - RoomNewMsgListener? msgGiftListener; - - RoomNewMsgListener? msgFloatingGiftListener; - RoomNewMsgListener? msgLuckyGiftRewardTickerListener; - RoomNewMsgListener? msgUserJoinListener; - - /// 当前会话 - V2TimConversation? currentConversation; - - /// 会话列表缓存 - Map conversationMap = {}; - - ///消息已读监听 - OnMessageRecvC2CReadListener? onMessageRecvC2CReadListener; - - ///消息被撤回监听 - OnRevokeMessageListener? onRevokeMessageListener; - - ///新消息监听 单聊 - OnNewMessageListener? onNewMessageCurrentConversationListener; - - OnNewANMessageListener? onNewActivityMessageCurrentConversationListener; - OnNewANMessageListener? onNewNotifcationMessageCurrentConversationListener; - - ///新消息监听 群聊 - Map onNewMessageListenerGroupMap = {}; - int allUnReadCount = 0; - int messageUnReadCount = 0; - int systemUnReadCount = 0; - int customerUnReadCount = 0; - int activityUnReadCount = 0; - int notifcationUnReadCount = 0; - SCBroadCastLuckGiftPush? currentPlayingLuckGift; - final Queue _luckGiftPushQueue = Queue(); - String? _currentLuckGiftPushKey; - Debouncer debouncer = Debouncer(); - List conversationList = []; - - ///客服 - SocialChatUserProfile? customerInfo; - - int _systemUnreadCount() { - int count = 0; - for (final conversationId in SCGlobalConfig.systemConversationIds) { - count += conversationMap[conversationId]?.unreadCount ?? 0; - } - return count; - } - - V2TimConversation getPreferredSystemConversation() { - final systemConversations = - conversationMap.values - .where( - (element) => - SCGlobalConfig.isSystemConversationId(element.conversationID), - ) - .toList() - ..sort((e1, e2) { - final time1 = e1.lastMessage?.timestamp ?? 0; - final time2 = e2.lastMessage?.timestamp ?? 0; - return time2.compareTo(time1); - }); - - if (systemConversations.isNotEmpty) { - return systemConversations.first; - } - - return V2TimConversation( - type: ConversationType.V2TIM_C2C, - userID: SCGlobalConfig.primarySystemUserId, - conversationID: SCGlobalConfig.primarySystemConversationId, - ); - } - - void getConversationList() { - List list = conversationMap.values.toList(); - list.removeWhere((element) { - if (element.conversationID == "c2c_${customerInfo?.id}") { - return true; - } - if (element.conversationID == "c2c_atyou-newsletter") { - ///删除这个会话,后台乱发的联系人。。防止无效未读数 - clearC2CHistoryMessage(element.conversationID, false); - return true; - } - if (element.lastMessage == null) { - return true; - } - return false; - }); - list.sort((e1, e2) { - int time1 = e1.lastMessage?.timestamp ?? 0; - int time2 = e2.lastMessage?.timestamp ?? 0; - return time2.compareTo(time1); - }); - conversationList = list; - systemUnReadCount = _systemUnreadCount(); - customerUnReadCount = - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; - - notifyListeners(); - } - - init(BuildContext context) async { - this.context = context; - V2TimSDKListener sdkListener = V2TimSDKListener( - onConnectFailed: (int code, String error) { - // 连接失败的回调函数 - // code 错误码 - // error 错误信息 - }, - onConnectSuccess: () { - // SDK 已经成功连接到腾讯云服务器 - }, - onConnecting: () { - // SDK 正在连接到腾讯云服务器 - }, - onKickedOffline: () { - // 当前用户被踢下线,此时可以 UI 提示用户,并再次调用 V2TIMManager 的 login() 函数重新登录。 - }, - onSelfInfoUpdated: (V2TimUserFullInfo info) { - // 登录用户的资料发生了更新 - // info登录用户的资料 - }, - onUserSigExpired: () { - // 在线时票据过期:此时您需要生成新的 userSig 并再次调用 V2TIMManager 的 login() 函数重新登录。 - }, - onUserStatusChanged: (List userStatusList) { - //用户状态变更通知 - //userStatusList 用户状态变化的用户列表 - //收到通知的情况:订阅过的用户发生了状态变更(包括在线状态和自定义状态),会触发该回调 - //在 IM 控制台打开了好友状态通知开关,即使未主动订阅,当好友状态发生变更时,也会触发该回调 - //同一个账号多设备登录,当其中一台设备修改了自定义状态,所有设备都会收到该回调 - }, - ); - V2TimValueCallback initSDKRes = await TencentImSDKPlugin.v2TIMManager - .initSDK( - sdkAppID: int.parse(SCGlobalConfig.tencentImAppid), // SDKAppID - loglevel: LogLevelEnum.V2TIM_LOG_ALL, // 日志登记等级 - listener: sdkListener, // 事件监听器 - ); - if (initSDKRes.code == 0) {} - try { - customerInfo = await SCConfigRepositoryImp().customerService(); - } catch (e) {} - - /// 登录 - await loginTencetRtm(context); - - /// 初始化会话列表 - // _onRefreshConversationSub = FTIM.getContactManager().addRefreshConversationListener(_onRefreshConversation); - TencentImSDKPlugin.v2TIMManager - .getConversationManager() - .addConversationListener( - listener: V2TimConversationListener( - onNewConversation: (conversationList) { - // _onRefreshConversation(conversationList); - initConversation(); - }, - onTotalUnreadMessageCountChanged: (int totalUnreadCount) { - messageUnReadCount = totalUnreadCount; - systemUnReadCount = _systemUnreadCount(); - customerUnReadCount = - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; - allUnReadCount = - messageUnReadCount + - notifcationUnReadCount + - activityUnReadCount; - notifyListeners(); - }, - ), - ); - TencentImSDKPlugin.v2TIMManager.addGroupListener( - listener: V2TimGroupListener( - onMemberEnter: (String groupID, List memberList) { - final rtcProvider = Provider.of( - context, - listen: false, - ); - if (groupID == - rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { - rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); - } - }, - onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { - final rtcProvider = Provider.of( - context, - listen: false, - ); - rtcProvider.removOnlineUser(groupID, member.userID!); - if (groupID == - rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { - rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); - } - }, - onMemberKicked: ( - String groupID, - V2TimGroupMemberInfo opUser, - List memberList, - ) { - ///踢出房间 - if (memberList.isNotEmpty) { - if (memberList.first.userID == - AccountStorage().getCurrentUser()?.userProfile?.id) { - Provider.of( - context, - listen: false, - ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - - ///退出房间 - Provider.of( - context, - listen: false, - ).exitCurrentVoiceRoomSession(false).whenComplete(() { - SCRoomUtils.closeAllDialogs(); - SmartDialog.show( - tag: "showConfirmDialog", - alignment: Alignment.center, - debounce: true, - animationType: SmartAnimationType.fade, - builder: (_) { - return MsgDialog( - title: SCAppLocalizations.of(context)!.tips, - msg: SCAppLocalizations.of(context)!.kickRoomTips, - btnText: SCAppLocalizations.of(context)!.confirm, - onEnsure: () {}, - ); - }, - ); - }); - } - } - }, - ), - ); - - /// 新消息监听 - // FTIM.getMessageManager().addNewMessagesListener(_onNewMessage); - TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( - listener: V2TimAdvancedMsgListener( - onRecvC2CReadReceipt: (List receiptList) { - //会话已读回调 - }, - onRecvMessageModified: (V2TimMessage message) { - // msg 为被修改之后的消息对象 - }, - onRecvMessageReadReceipts: (List receiptList) { - //群聊/单聊已读回调 - List messageIDList = []; - for (var element in receiptList) { - messageIDList.add(element.msgID!); - } - onMessageRecvC2CReadListener?.call(messageIDList); - }, - onRecvMessageRevoked: (String messageId) { - // 在本地维护的消息中处理被对方撤回的消息 - TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .deleteMessages(msgIDs: [messageId]) - .then((result) { - onRevokeMessageListener?.call(messageId); - for (var ms in conversationList) { - if (ms.lastMessage?.msgID == messageId) { - ms.lastMessage?.status = - MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED; - break; - } - } - notifyListeners(); - }); - }, - onRecvNewMessage: (V2TimMessage message) async { - _onNewMessage(message); - }, - onSendMessageProgress: (V2TimMessage message, int progress) { - //文件上传进度回调 - }, - ), - ); - getAllUnReadCount(); - joinBigBroadcastGroup(); - } - - /// 初始化会话 - /// 打开聊天界面的时候调用 - int sTime = 0; - - Future startConversation(V2TimConversation conversation) async { - assert(conversation != null); - int eTime = DateTime.now().millisecondsSinceEpoch; - if (eTime - sTime > 5000) { - sTime = eTime; - currentConversation = conversation; - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .markC2CMessageAsRead(userID: conversation.userID!); - notifyListeners(); - return true; - } else { - return false; - } - } - - ///初始话会话 - Future initConversation() async { - V2TimValueCallback convList = - await TencentImSDKPlugin.v2TIMManager - .getConversationManager() - .getConversationList(nextSeq: '0', count: 100); - List? conversationList = - convList.data?.conversationList; - print('conversationList:${conversationList?.length}'); - conversationMap.clear(); - if (conversationList != null) { - for (V2TimConversation? conversation in conversationList) { - conversationMap[conversation!.conversationID] = conversation; - } - } - getConversationList(); - } - - // /// 所有消息未读数 - // int getAllUnReadCount() { - // allUnReadCount = 0; - // for (var value in conversationList) { - // allUnReadCount += value.unreadCount ?? 0; - // } - // // for (var value in systemConversationList) { - // // i += value.unreadMessageNum; - // // } - // notifyListeners(); - // return allUnReadCount; - // } - - /// 所有消息未读数 - void getAllUnReadCount() async { - V2TimValueCallback res = - await TencentImSDKPlugin.v2TIMManager - .getConversationManager() - .getTotalUnreadMessageCount(); - if (res.code == 0) { - print('初始未读总数: ${res.data}'); - messageUnReadCount = res.data ?? 0; - allUnReadCount = - messageUnReadCount + activityUnReadCount + notifcationUnReadCount; - notifyListeners(); - // 这里可以先用初始值更新UI - } - } - - ///登录IM - Future loginTencetRtm(BuildContext context) async { - SocialChatLoginRes? userModel = AccountStorage().getCurrentUser(); - bool logined = false; - - while (!logined && userModel != null) { - await Future.delayed(Duration(milliseconds: 550)); - try { - if (userModel.userSig != null) { - V2TimCallback res = await TencentImSDKPlugin.v2TIMManager.login( - userID: userModel.userProfile?.id ?? "", - userSig: userModel.userSig ?? "", - ); - print( - 'tim voLogin:${res.code},${userModel.userProfile?.id},${userModel.userSig}', - ); - if (res.code == 0) { - isLogout = false; - // 登录成功逻辑 - logined = true; - print('tim 登录成功'); - await initConversation(); - } else { - // 登录失败逻辑 - //print('timm 需要重新登录2'); - print('tim 登录失败${res.code}'); - SCTts.show('tim login fail'); - } - } else { - //print('timm 需要重新登录sign'); - SCTts.show('tim login fail'); - } - } catch (e) { - //print('timm 登录异常:${e.toString()}'); - SCTts.show('timm login fail:${e.toString()}'); - } - userModel = AccountStorage().getCurrentUser(); - } - } - - _onNewMessage(V2TimMessage message) { - if (message.groupID != null) { - ///全服通知 - if (message.groupID == SCGlobalConfig.bigBroadcastGroup) { - _newBroadCastMsgRecv(message.groupID!, message); - } - - ///群消息 - for (var element in onNewMessageListenerGroupMap.values) { - element?.call(message.groupID!, message); - } - } else { - ///单聊消息 - _onNew1v1Message(message); - } - } - - _onNew1v1Message(V2TimMessage? message, {String? msgId}) async { - for (var element in conversationList) { - if (message?.userID == element?.userID) { - element.lastMessage = message; - if (onNewMessageCurrentConversationListener == null) { - element.unreadCount = element.unreadCount! + 1; - } - } - } - if (message?.userID == customerInfo?.id) { - if (onNewMessageCurrentConversationListener == null) { - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = - (conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0) + 1; - } - } - systemUnReadCount = _systemUnreadCount(); - notifyListeners(); - onNewMessageCurrentConversationListener?.call(message, msgId: msgId); - } - - void _onRefreshConversation(List conversations) { - for (V2TimConversation conversation in conversations) { - conversationMap[conversation.conversationID] = conversation; - } - getConversationList(); - } - - ///创建房间im群聊 - Future> createRoomGroup( - String groupID, - String groupName, - ) { - return V2TIMGroupManager().createGroup( - groupID: groupID, - groupType: GroupType.AVChatRoom, - groupName: groupName, - ); - } - - ///加入房间的im群聊 - Future joinRoomGroup(String groupID, String message) async { - _luckGiftPushQueue.clear(); - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; - var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( - groupID: groupID, - message: message, - ); - if (joinResult.code == 0) { - onNewMessageListenerGroupMap[groupID] = _newGroupMsg; - } - return joinResult; - } - - ///发送文本消息 - Future dispatchMessage( - Msg msg, { - bool showEmoticons = true, - bool addLocal = true, - }) async { - if (addLocal) { - addMsg(msg); - } - // 发送消息到腾讯云IM - if (msg.groupId != null) { - await _sendTencentMessage(msg); - } - notifyListeners(); - } - - // 发送腾讯云消息 - Future _sendTencentMessage(Msg msg) async { - try { - switch (msg.type) { - case SCRoomMsgType.text: - case SCRoomMsgType.shangMai: - case SCRoomMsgType.emoticons: - case SCRoomMsgType.xiaMai: - case SCRoomMsgType.killXiaMai: - case SCRoomMsgType.roomRoleChange: - case SCRoomMsgType.roomSettingUpdate: - case SCRoomMsgType.roomBGUpdate: - case SCRoomMsgType.qcfj: - case SCRoomMsgType.fengMai: - case SCRoomMsgType.jieFeng: - case SCRoomMsgType.joinRoom: - case SCRoomMsgType.gift: - case SCRoomMsgType.bsm: - case SCRoomMsgType.roomDice: - case SCRoomMsgType.roomRPS: - case SCRoomMsgType.roomLuckNumber: - case SCRoomMsgType.image: - case SCRoomMsgType.roomGameClose: - case SCRoomMsgType.roomGameCreate: - case SCRoomMsgType.luckGiftAnimOther: - _sendRoomMessage(msg); - break; - default: - break; - } - } catch (e) { - print('发送消息失败: $e'); - // 处理发送失败的情况 - } - } - - ///发送单聊文本消息 - Future sendC2CTextMsg( - String msg, - V2TimConversation toConversation, - ) async { - // 创建文本消息 - V2TimValueCallback createTextMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createTextMessage( - text: msg, // 文本信息 - ); - if (createTextMessageRes.code == 0) { - // 文本信息创建成功 - String id = createTextMessageRes.data!.id!; - // 发送文本消息 - // 在sendMessage时,若只填写receiver则发个人用户单聊消息 - // 若只填写groupID则发群组消息 - // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 - String receiveId = toConversation.userID!; - V2TimValueCallback sendMessageRes = await TencentImSDKPlugin - .v2TIMManager - .getMessageManager() - .sendMessage( - id: id, // 创建的messageid - receiver: toConversation.userID!, // 接收人id - needReadReceipt: true, - groupID: '', // 是否需要已读回执 - ); - if (sendMessageRes.code == 0) { - // 发送成功 - _onNew1v1Message(sendMessageRes.data); - } else { - SCTts.show( - 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', - ); - } - } - } - - ///发送单聊自定义消息 - Future sendC2CCustomMsg( - String msg, - V2TimConversation toConversation, - String extension, - ) async { - // 创建文本消息 - V2TimValueCallback createCustomMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createCustomMessage( - data: msg, // 文本信息 - extension: extension, - ); - if (createCustomMessageRes.code == 0) { - // 文本信息创建成功 - String id = createCustomMessageRes.data!.id!; - // 发送文本消息 - // 在sendMessage时,若只填写receiver则发个人用户单聊消息 - // 若只填写groupID则发群组消息 - // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 - String receiveId = toConversation.userID!; - V2TimValueCallback sendMessageRes = await TencentImSDKPlugin - .v2TIMManager - .getMessageManager() - .sendMessage( - id: id, - // 创建的messageid - receiver: toConversation.userID!, - // 接收人id - needReadReceipt: true, - isSupportMessageExtension: true, - groupID: '', // 是否需要已读回执 - ); - if (sendMessageRes.code == 0) { - // 发送成功 - _onNew1v1Message(sendMessageRes.data); - } else { - SCTts.show( - 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', - ); - } - } - } - - ///发送单聊图片消息 - Future sendImageMsg({ - List? selectedList, - File? file, - required V2TimConversation conversation, - }) async { - if (file != null) { - File newFile = await SCMessageUtils.createImageElem(file); - V2TimValueCallback createImageMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createImageMessage(imagePath: newFile.path); - if (createImageMessageRes.code == 0) { - String id = createImageMessageRes.data!.id!; - V2TimValueCallback sendMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .sendMessage( - id: id, - receiver: conversation.userID!, - needReadReceipt: true, - groupID: '', - ); - if (sendMessageRes.code == 0) { - // 发送成功 - _onNew1v1Message(sendMessageRes.data); - } else { - SCTts.show( - 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', - ); - } - } - } else { - if (selectedList != null) { - for (File entity in selectedList) { - String id = ""; - V2TimMessage? message; - //判断是视频或者图片消息 - - if (SCPathUtils.getFileType(entity.path) == "image") { - V2TimValueCallback createImageMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createImageMessage(imagePath: entity.path); - if (createImageMessageRes.code == 0) { - message = createImageMessageRes.data?.messageInfo; - id = createImageMessageRes.data!.id!; - // 创建图片 - File newFile = await SCMessageUtils.createImageElem(entity); - //发送 - V2TimImageElem elem = V2TimImageElem(path: newFile.path); - message?.imageElem = elem; - } - } else if (SCPathUtils.getFileType(entity.path) == "video_pic") { - if (entity.lengthSync() > 50000000) { - SCTts.show( - SCAppLocalizations.of(context!)!.theVideoSizeCannotExceed, - ); - return; - } - // 复制一份视频 - String md5Str1 = keyToMd5(entity.path); - File newFile = File( - "${FileCacheManager.videoCachePath}/$md5Str1.mp4", - ); - if (!newFile.existsSync()) { - await entity.copy(newFile.path); - } - // 创建缩略图 - String? thumbImagePath = await SCMessageUtils.generateFileThumbnail( - newFile.path, - 128, - ); - V2TimValueCallback createVideoMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createVideoMessage( - videoFilePath: entity.path, - type: "mp4", - duration: 0, - snapshotPath: thumbImagePath ?? "", - ); - - if (createVideoMessageRes.code == 0) { - message = createVideoMessageRes.data?.messageInfo; - id = createVideoMessageRes.data!.id!; - } - } - // 消息设置 - message?.isSelf = true; - // message.conversation = conversation; - message?.status = MessageStatus.V2TIM_MSG_STATUS_SENDING; - print('组装完成,准备发送:${message?.toJson()}'); - // String id = await FTIM.getMessageManager().sendMessage(message); - // message.msgId = id; - V2TimValueCallback sendMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .sendMessage( - id: id, - receiver: conversation.userID!, - needReadReceipt: true, - groupID: '', - ); - if (sendMessageRes.code == 0) { - // 发送成功 - _onNew1v1Message(sendMessageRes.data); - } else { - SCTts.show( - 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', - ); - } - } - } - } - notifyListeners(); - } - - // 发送消息(房间) - Future _sendRoomMessage(Msg msg) async { - try { - if (msg.type == SCRoomMsgType.luckGiftAnimOther) { - _giftFxLog( - 'send room msg start ' - 'type=${msg.type} ' - 'groupId=${msg.groupId} ' - 'msg=${msg.msg} ' - 'giftId=${msg.gift?.id} ' - 'giftPhoto=${msg.gift?.giftPhoto}', - ); - } - if (msg.type == SCRoomMsgType.roomSettingUpdate) { - debugPrint( - "[Room Cover Sync] send roomSettingUpdate groupId=${msg.groupId ?? ""} roomId=${msg.msg ?? ""}", - ); - } - var user = msg.user?.copyWith(); - var toUser = msg.toUser?.copyWith(); - user?.cleanWearHonor(); - user?.cleanWearBadge(); - user?.cleanPhotos(); - toUser?.cleanWearHonor(); - toUser?.cleanWearBadge(); - toUser?.cleanUseProps(); - toUser?.cleanPhotos(); - msg.needUpDataUserInfo = - Provider.of( - context!, - listen: false, - ).needUpDataUserInfo; - msg.user = user; - msg.toUser = toUser; - final textMsg = await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createCustomMessage(data: jsonEncode(msg.toJson())); - - if (msg.type == SCRoomMsgType.luckGiftAnimOther) { - _giftFxLog( - 'send room msg createCustomMessage ' - 'type=${msg.type} ' - 'code=${textMsg.code} ' - 'id=${textMsg.data?.id} ' - 'desc=${textMsg.desc}', - ); - } - - if (textMsg.code != 0) return; - - final sendResult = await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .sendMessage( - id: textMsg.data!.id!, - groupID: msg.groupId!, - receiver: '', - ); - if (msg.type == SCRoomMsgType.luckGiftAnimOther) { - _giftFxLog( - 'send room msg result ' - 'type=${msg.type} ' - 'code=${sendResult.code} ' - 'msgId=${sendResult.data?.msgID} ' - 'groupId=${sendResult.data?.groupID} ' - 'desc=${sendResult.desc}', - ); - } - if (sendResult.code == 0) {} - } catch (e) { - throw Exception("create fail: $e"); - } - } - - /// 添加消息 - addMsg(Msg msg) { - final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg); - if (mergedGiftMsg != null) { - msgAllListener?.call(mergedGiftMsg); - msgGiftListener?.call(mergedGiftMsg); - if (msg.type == SCRoomMsgType.gift) { - msgFloatingGiftListener?.call(msg); - } - notifyListeners(); - return; - } - - roomAllMsgList.insert(0, msg); - if (roomAllMsgList.length > 250) { - print('大于200条消息'); - roomAllMsgList.removeAt(roomAllMsgList.length - 1); - } - msgAllListener?.call(msg); - - if (msg.type == SCRoomMsgType.text) { - roomChatMsgList.insert(0, msg); - if (roomChatMsgList.length > 250) { - print('大于200条消息'); - roomChatMsgList.removeAt(roomChatMsgList.length - 1); - } - msgChatListener?.call(msg); - } else if (msg.type == SCRoomMsgType.image) { - roomChatMsgList.insert(0, msg); - if (roomChatMsgList.length > 250) { - print('大于200条消息'); - roomChatMsgList.removeAt(roomChatMsgList.length - 1); - } - msgChatListener?.call(msg); - } else if (msg.type == SCRoomMsgType.gift || - msg.type == SCRoomMsgType.luckGiftAnimOther) { - roomGiftMsgList.insert(0, msg); - if (roomGiftMsgList.length > 250) { - print('大于200条消息'); - roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); - } - msgGiftListener?.call(msg); - if (msg.type == SCRoomMsgType.gift) { - msgFloatingGiftListener?.call(msg); - } - } - } - - Msg? _mergeGiftMessageIfNeeded(Msg incoming) { - if (incoming.type != SCRoomMsgType.gift && - incoming.type != SCRoomMsgType.luckGiftAnimOther) { - return null; - } - - final mergeTarget = _findMergeableGiftMessage(incoming); - if (mergeTarget == null) { - return null; - } - - mergeTarget.number = (mergeTarget.number ?? 0) + (incoming.number ?? 0); - mergeTarget.time = DateTime.now().millisecondsSinceEpoch; - if ((incoming.msg ?? "").trim().isNotEmpty) { - mergeTarget.msg = incoming.msg; - } - - _moveMessageToFront(roomGiftMsgList, mergeTarget); - _moveMessageToFront(roomAllMsgList, mergeTarget); - return mergeTarget; - } - - Msg? _findMergeableGiftMessage(Msg incoming) { - final now = DateTime.now().millisecondsSinceEpoch; - for (final existing in roomGiftMsgList) { - if ((existing.time ?? 0) <= 0 || - now - (existing.time ?? 0) > _giftComboMergeWindowMs) { - continue; - } - if (_isSameGiftComboMessage(existing, incoming)) { - return existing; - } - } - return null; - } - - bool _isSameGiftComboMessage(Msg existing, Msg incoming) { - return existing.type == incoming.type && - existing.groupId == incoming.groupId && - existing.user?.id == incoming.user?.id && - existing.toUser?.id == incoming.toUser?.id && - existing.gift?.id == incoming.gift?.id; - } - - void _moveMessageToFront(List messages, Msg target) { - final index = messages.indexOf(target); - if (index <= 0) { - return; - } - messages.removeAt(index); - messages.insert(0, target); - } - - bool _shouldHighlightLuckyGiftReward(SCBroadCastLuckGiftPush broadCastRes) { - final rewardData = broadCastRes.data; - if (rewardData == null) { - return false; - } - if (rewardData.isBigReward) { - return true; - } - return (rewardData.multiple ?? 0) >= 5; - } - - bool _isLuckyGiftInCurrentRoom(SCBroadCastLuckGiftPush broadCastRes) { - if (context == null) { - return false; - } - final currentRoomId = - Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.id ?? - ''; - final roomId = broadCastRes.data?.roomId ?? ''; - return currentRoomId.isNotEmpty && - roomId.isNotEmpty && - currentRoomId == roomId; - } - - SCFloatingMessage _buildLuckyGiftFloatingMessage( - SCBroadCastLuckGiftPush broadCastRes, - ) { - final rewardData = broadCastRes.data; - return SCFloatingMessage( - type: 0, - userId: rewardData?.sendUserId, - roomId: rewardData?.roomId, - toUserId: rewardData?.acceptUserId, - userAvatarUrl: rewardData?.userAvatar, - userName: rewardData?.nickname, - toUserName: rewardData?.acceptNickname, - giftUrl: rewardData?.giftCover, - number: rewardData?.giftQuantity, - coins: rewardData?.awardAmount, - multiple: rewardData?.multiple, - priority: 1000, - ); - } - - void _handleLuckyGiftGlobalNews( - SCBroadCastLuckGiftPush broadCastRes, { - required String source, - }) { - final rewardData = broadCastRes.data; - if (rewardData == null) { - return; - } - if (_isLuckyGiftInCurrentRoom(broadCastRes)) { - addluckGiftPushQueue(broadCastRes); - } - if (!rewardData.shouldShowGlobalNews || - (rewardData.multiple ?? 0) < _luckyGiftFloatMinMultiple) { - return; - } - if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) { - _giftFxLog( - 'skip global lucky gift overlay ' - 'reason=current_room_already_receives_group_msg ' - 'roomId=${rewardData.roomId} ' - 'giftId=${rewardData.giftId}', - ); - return; - } - OverlayManager().addMessage(_buildLuckyGiftFloatingMessage(broadCastRes)); - } - - void _handleRoomLuckyGiftMessage(SCBroadCastLuckGiftPush broadCastRes) { - final rewardData = broadCastRes.data; - if (rewardData == null) { - return; - } - final roomMsg = Msg( - groupId: '', - msg: '', - type: SCRoomMsgType.gameLuckyGift, - ); - roomMsg.gift = SocialChatGiftRes( - id: rewardData.giftId, - giftPhoto: rewardData.giftCover, - giftTab: 'LUCK', - ); - roomMsg.number = 0; - roomMsg.awardAmount = rewardData.awardAmount; - roomMsg.user = SocialChatUserProfile( - id: rewardData.sendUserId, - userNickname: rewardData.nickname, - userAvatar: rewardData.userAvatar, - ); - roomMsg.toUser = SocialChatUserProfile( - id: rewardData.acceptUserId, - userNickname: rewardData.acceptNickname, - ); - addMsg(roomMsg); - msgLuckyGiftRewardTickerListener?.call(roomMsg); - - if (_shouldHighlightLuckyGiftReward(broadCastRes)) { - final highlightMsg = Msg( - groupId: '', - msg: '${rewardData.multiple ?? 0}', - type: SCRoomMsgType.gameLuckyGift_5, - ); - highlightMsg.awardAmount = rewardData.awardAmount; - highlightMsg.user = SocialChatUserProfile( - id: rewardData.sendUserId, - userNickname: rewardData.nickname, - ); - addMsg(highlightMsg); - } - - addluckGiftPushQueue(broadCastRes); - _handleLuckyGiftGlobalNews(broadCastRes, source: 'room_group'); - - if (rewardData.sendUserId == - AccountStorage().getCurrentUser()?.userProfile?.id) { - Provider.of( - context!, - listen: false, - ).updateLuckyRewardAmount(roomMsg.awardAmount ?? 0); - } - } - - bool isLogout = false; - - logout() async { - V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout(); - TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .removeAdvancedMsgListener(); - if (logoutRes.code == 0) { - isLogout = true; - } - } - - ///全服广播消息 - _newBroadCastMsgRecv(String groupID, V2TimMessage message) async { - try { - String? customData = message.customElem?.data; - if (customData != null && customData.isNotEmpty) { - final data = json.decode(customData); - var type = data["type"]; - if (type == "SYS_ACTIVITY") { - if (onNewActivityMessageCurrentConversationListener != null) { - var recode = Records.fromJson(data["data"]); - onNewActivityMessageCurrentConversationListener?.call(recode); - } else { - activityUnReadCount = activityUnReadCount + 1; - allUnReadCount = - messageUnReadCount + - notifcationUnReadCount + - activityUnReadCount; - notifyListeners(); - } - } else if (type == "SYS_ANNOUNCEMENT") { - if (onNewNotifcationMessageCurrentConversationListener != null) { - var recode = Records.fromJson(data["data"]); - onNewNotifcationMessageCurrentConversationListener?.call(recode); - } else { - notifcationUnReadCount = notifcationUnReadCount + 1; - allUnReadCount = - messageUnReadCount + - notifcationUnReadCount + - activityUnReadCount; - notifyListeners(); - } - } else if (type == "GAME_BAISHUN_WIN") { - if (SCGlobalConfig.isReview) { - ///审核状态不播放动画 - return; - } - var fdata = data["data"]; - var winCoins = fdata["currencyDiff"]; - if (winCoins > 14999) { - ///达到5000才飘屏 - SCFloatingMessage msg = SCFloatingMessage( - type: 2, - userId: fdata["account"], - userAvatarUrl: fdata["userAvatar"], - userName: fdata["userNickname"], - giftUrl: fdata["gameUrl"], - roomId: fdata["roomId"], - coins: fdata["currencyDiff"], - ); - OverlayManager().addMessage(msg); - } - } else if (type == "GAME_LUCKY_GIFT") { - final broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); - _giftFxLog( - 'recv GAME_LUCKY_GIFT broadcast ' - 'giftId=${broadCastRes.data?.giftId} ' - 'roomId=${broadCastRes.data?.roomId} ' - 'sendUserId=${broadCastRes.data?.sendUserId} ' - 'acceptUserId=${broadCastRes.data?.acceptUserId} ' - 'giftQuantity=${broadCastRes.data?.giftQuantity} ' - 'awardAmount=${broadCastRes.data?.awardAmount} ' - 'multiple=${broadCastRes.data?.multiple} ' - 'multipleType=${broadCastRes.data?.multipleType} ' - 'globalNews=${broadCastRes.data?.globalNews}', - ); - _handleLuckyGiftGlobalNews(broadCastRes, source: 'broadcast'); - } else if (type == "REGISTER_REWARD_GRANTED") { - await DataPersistence.setPendingRegisterRewardDialog(true); - await DataPersistence.clearAwaitRegisterRewardSocket(); - eventBus.fire(RegisterRewardGrantedEvent(data: data["data"])); - } else if (type == "ROCKET_ENERGY_LAUNCH") { - ///火箭触发飘屏 - var fdata = data["data"]; - SCFloatingMessage msg = SCFloatingMessage( - type: 3, - roomId: fdata["roomId"], - rocketLevel: fdata["fromLevel"], - userAvatarUrl: fdata["userAvatar"], - userName: fdata["nickname"], - userId: fdata["actualAccount"], - priority: 1000, - ); - OverlayManager().addMessage(msg); - } else if (type == SCRoomMsgType.roomRedPacket) { - ///红包触发飘屏 - var fData = data["data"]; - SCFloatingMessage msg = SCFloatingMessage( - type: 4, - roomId: fData["roomId"], - userAvatarUrl: fData["userAvatar"], - userName: fData["userNickname"], - userId: fData["actualAccount"], - toUserId: fData["packetId"], - priority: 1000, - ); - if (msg.roomId == - Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.id) { - Provider.of( - context!, - listen: false, - ).loadRoomRedPacketList(1); - } - OverlayManager().addMessage(msg); - } else if (type == SCRoomMsgType.inviteRoom) { - ///邀请进入房间 - var fdata = data["data"]; - SCFloatingMessage msg = SCFloatingMessage.fromJson(fdata); - if (msg.toUserId == - AccountStorage().getCurrentUser()?.userProfile?.id && - msg.roomId != - Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.id) { - SmartDialog.dismiss(tag: "showInviteRoom"); - SmartDialog.show( - tag: "showInviteRoom", - alignment: Alignment.center, - animationType: SmartAnimationType.fade, - builder: (_) { - return InviteRoomDialog(msg); - }, - ); - } - } - } - } catch (e) {} - } - - _newGroupMsg(String groupID, V2TimMessage message) { - if (groupID != - Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.roomAccount) { - return; - } - try { - String? customData = message.customElem?.data; - if (customData != null && customData.isNotEmpty) { - // 直接处理字符串格式的自定义数据 - final data = json.decode(customData); - debugPrint(">>>>>>>>>>>>>>>>>>>消息类型${data["type"]}"); - - if (data["type"] == SCRoomMsgType.roomRedPacket) { - ///房间红包 - var fData = data["data"]; - SCFloatingMessage msg = SCFloatingMessage( - type: 4, - roomId: fData["roomId"], - userAvatarUrl: fData["userAvatar"], - userName: fData["userNickname"], - userId: fData["actualAccount"], - toUserId: fData["packetId"], - priority: 1000, - ); - Provider.of( - context!, - listen: false, - ).loadRoomRedPacketList(1); - OverlayManager().addMessage(msg); - return; - } - Msg msg = Msg.fromJson(data); - - if (msg.type == SCRoomMsgType.sendGift || - msg.type == SCRoomMsgType.gameBurstCrystalSprint || - msg.type == SCRoomMsgType.gameBurstCrystalBox) { - ///这个消息暂时不监听 - return; - } - if (msg.type == SCRoomMsgType.bsm) { - if (msg.toUser?.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { - SmartDialog.show( - tag: "showConfirmDialog", - alignment: Alignment.center, - debounce: true, - animationType: SmartAnimationType.fade, - builder: (_) { - return MsgDialog( - title: SCAppLocalizations.of(context!)!.tips, - msg: SCAppLocalizations.of( - context!, - )!.invitesYouToTheMicrophone(msg.msg ?? ""), - btnText: SCAppLocalizations.of(context!)!.confirm, - onEnsure: () { - ///上麦 - num index = - Provider.of( - context!, - listen: false, - ).findWheat(); - if (index > -1) { - Provider.of( - context!, - listen: false, - ).shangMai( - index, - eventType: "INVITE", - inviterId: msg.role, - ); - } - }, - ); - }, - ); - } - return; - } - if (msg.type == SCRoomMsgType.killXiaMai) { - ///踢下麦 - if (msg.msg == AccountStorage().getCurrentUser()?.userProfile?.id) { - Provider.of( - context!, - listen: false, - ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - } - Provider.of( - context!, - listen: false, - ).retrieveMicrophoneList(notifyIfUnchanged: false); - return; - } - - if (msg.type == SCRoomMsgType.roomSettingUpdate) { - debugPrint( - "[Room Cover Sync] recv roomSettingUpdate groupId=$groupID roomId=${msg.msg ?? ""}", - ); - Provider.of( - context!, - listen: false, - ).loadRoomInfo(msg.msg ?? ""); - return; - } - if (msg.type == SCRoomMsgType.roomBGUpdate) { - SCRoomThemeListRes res; - if ((msg.msg ?? "").isNotEmpty) { - res = SCRoomThemeListRes.fromJson(jsonDecode(msg.msg!)); - } else { - res = SCRoomThemeListRes(); - } - Provider.of( - context!, - listen: false, - ).updateRoomBG(res); - return; - } - if (msg.type == SCRoomMsgType.emoticons) { - Provider.of( - context!, - listen: false, - ).starPlayEmoji(msg); - return; - } - if (msg.type == SCRoomMsgType.micChange) { - Provider.of( - context!, - listen: false, - ).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); - } else if (msg.type == SCRoomMsgType.shangMai || - msg.type == SCRoomMsgType.xiaMai || - msg.type == SCRoomMsgType.fengMai || - msg.type == SCRoomMsgType.jieFeng) { - Provider.of( - context!, - listen: false, - ).retrieveMicrophoneList(notifyIfUnchanged: false); - } else if (msg.type == SCRoomMsgType.refreshOnlineUser) { - Provider.of( - context!, - listen: false, - ).fetchOnlineUsersList(notifyIfUnchanged: false); - } else if (msg.type == SCRoomMsgType.gameLuckyGift) { - var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); - _giftFxLog( - 'recv GAME_LUCKY_GIFT ' - 'giftId=${broadCastRes.data?.giftId} ' - 'roomId=${broadCastRes.data?.roomId} ' - 'sendUserId=${broadCastRes.data?.sendUserId} ' - 'acceptUserId=${broadCastRes.data?.acceptUserId} ' - 'giftQuantity=${broadCastRes.data?.giftQuantity} ' - 'awardAmount=${broadCastRes.data?.awardAmount} ' - 'multiple=${broadCastRes.data?.multiple} ' - 'multipleType=${broadCastRes.data?.multipleType} ' - 'globalNews=${broadCastRes.data?.globalNews}', - ); - _handleRoomLuckyGiftMessage(broadCastRes); - } else { - if (msg.type == SCRoomMsgType.joinRoom) { - final shouldShowRoomVisualEffects = - Provider.of( - context!, - listen: false, - ).shouldShowRoomVisualEffects; - if (msg.user != null) { - Provider.of( - context!, - listen: false, - ).addOnlineUser(msg.groupId ?? "", msg.user!); - } - if (msgUserJoinListener != null) { - msgUserJoinListener!(msg); - } - - ///坐骑 - if (msg.user?.getMountains() != null) { - if (SCGlobalConfig.isEntryVehicleAnimation && - shouldShowRoomVisualEffects) { - SCGiftVapSvgaManager().play( - msg.user?.getMountains()?.sourceUrl ?? "", - priority: 100, - type: 1, - ); - } - } - } else if (msg.type == SCRoomMsgType.gift) { - final gift = msg.gift; - if (gift == null) { - _giftFxLog( - 'recv gift msg skipped reason=no_gift ' - 'fromUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'quantity=${msg.number}', - ); - } else { - final rtcProvider = Provider.of( - context!, - listen: false, - ); - final special = gift.special ?? ""; - final giftSourceUrl = gift.giftSourceUrl ?? ""; - final hasSource = giftSourceUrl.isNotEmpty; - final hasAnimation = scGiftHasAnimationSpecial(special); - final hasGlobalGift = special.contains( - SCGiftType.GLOBAL_GIFT.name, - ); - final hasFullScreenEffect = scGiftHasFullScreenEffect(special); - _giftFxLog( - 'recv gift msg ' - 'fromUserId=${msg.user?.id} ' - 'fromUserName=${msg.user?.userNickname} ' - 'toUserId=${msg.toUser?.id} ' - 'toUserName=${msg.toUser?.userNickname} ' - 'giftId=${gift.id} ' - 'giftName=${gift.giftName} ' - 'giftSourceUrl=$giftSourceUrl ' - 'special=$special ' - 'hasSource=$hasSource ' - 'hasAnimation=$hasAnimation ' - 'hasGlobalGift=$hasGlobalGift ' - 'hasFullScreenEffect=$hasFullScreenEffect ' - 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', - ); - if (giftSourceUrl.isNotEmpty && special.isNotEmpty) { - if (scGiftHasFullScreenEffect(special)) { - if (SCGlobalConfig.isGiftSpecialEffects && - rtcProvider.shouldShowRoomVisualEffects) { - _giftFxLog( - 'trigger player play path=$giftSourceUrl ' - 'giftId=${gift.id} giftName=${gift.giftName}', - ); - SCGiftVapSvgaManager().play(giftSourceUrl); - } else { - _giftFxLog( - 'skip player play because visual effects disabled ' - 'giftId=${gift.id} ' - 'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} ' - 'roomVisible=${rtcProvider.shouldShowRoomVisualEffects}', - ); - } - } else { - _giftFxLog( - 'skip player play because special does not include ' - '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' - 'giftId=${gift.id} special=${gift.special}', - ); - } - } else { - _giftFxLog( - 'skip player play because giftSourceUrl or special is empty ' - 'giftId=${gift.id} ' - 'giftSourceUrl=${gift.giftSourceUrl} ' - 'special=${gift.special}', - ); - } - if (rtcProvider - .currenRoom - ?.roomProfile - ?.roomSetting - ?.showHeartbeat ?? - false) { - debouncer.debounce( - duration: Duration(milliseconds: 350), - onDebounce: () { - rtcProvider.requestGiftTriggeredMicRefresh(); - }, - ); - } - final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0); - if (coins > 9999) { - OverlayManager().addMessage( - SCFloatingMessage( - type: 1, - userAvatarUrl: msg.user?.userAvatar ?? "", - userName: msg.user?.userNickname ?? "", - toUserName: msg.toUser?.userNickname ?? "", - toUserAvatarUrl: msg.toUser?.userAvatar ?? "", - giftUrl: gift.giftPhoto, - number: msg.number, - coins: coins, - roomId: msg.msg, - ), - ); - } - } - } else if (msg.type == SCRoomMsgType.luckGiftAnimOther) { - final hideLGiftAnimal = - Provider.of( - context!, - listen: false, - ).hideLGiftAnimal; - if (hideLGiftAnimal) { - _giftFxLog( - 'recv LUCK_GIFT_ANIM_OTHER skipped ' - 'reason=hideLGiftAnimal ' - 'giftPhoto=${msg.gift?.giftPhoto}', - ); - } else { - final targetUserIds = - (jsonDecode(msg.msg ?? "") as List) - .map((e) => e as String) - .toList(); - _giftFxLog( - 'recv LUCK_GIFT_ANIM_OTHER ' - 'giftPhoto=${msg.gift?.giftPhoto} ' - 'sendUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'quantity=${msg.number} ' - 'targetUserIds=${targetUserIds.join(",")}', - ); - eventBus.fire( - GiveRoomLuckWithOtherEvent( - msg.gift?.giftPhoto ?? "", - targetUserIds, - ), - ); - if (msg.user != null && msg.toUser != null && msg.gift != null) { - _giftFxLog( - 'trigger floating gift listener from LUCK_GIFT_ANIM_OTHER ' - 'sendUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'quantity=${msg.number} ' - 'giftId=${msg.gift?.id}', - ); - msgFloatingGiftListener?.call(msg); - } else { - _giftFxLog( - 'skip floating gift listener from LUCK_GIFT_ANIM_OTHER ' - 'reason=incomplete_msg ' - 'sendUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'giftId=${msg.gift?.id} ' - 'quantity=${msg.number}', - ); - } - } - } else if (msg.type == SCRoomMsgType.roomRoleChange) { - ///房间身份变动 - Provider.of( - context!, - listen: false, - ).retrieveMicrophoneList(); - if (msg.toUser?.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { - Provider.of( - context!, - listen: false, - ).currenRoom?.entrants?.setRoles(msg.msg); - if (msg.msg == SCRoomRolesType.TOURIST.name && - !(Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomSetting?.touristMike ?? - false)) { - ///如果变成了游客,房间又是禁止游客上麦,需要下麦 - num index = Provider.of( - context!, - listen: false, - ).userOnMaiInIndex( - AccountStorage().getCurrentUser()?.userProfile?.id ?? "", - ); - if (index > -1) { - Provider.of( - context!, - listen: false, - ).xiaMai(index); - } - } - } - } else if (msg.type == SCRoomMsgType.roomDice) { - if ((msg.number ?? -1) > -1) { - Provider.of( - context!, - listen: false, - ).starPlayEmoji(msg); - } - } else if (msg.type == SCRoomMsgType.roomRPS) { - if ((msg.number ?? -1) > -1) { - Provider.of( - context!, - listen: false, - ).starPlayEmoji(msg); - } - } - addMsg(msg); - } - } - } catch (e) { - throw Exception("message parser fail: $e"); - } - } - - ///加入全服广播群 - joinBigBroadcastGroup() async { - bool joined = false; - while (!isLogout && !joined) { - await Future.delayed(Duration(milliseconds: 550)); - try { - var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( - groupID: SCGlobalConfig.bigBroadcastGroup, - message: "", - ); - if (joinResult.code == 0) { - joined = true; - } - } catch (e) { - //print('timm 登录异常:${e.toString()}'); - SCTts.show('broadcastGroup join fail:${e.toString()}'); - } - } - } - - ///发送全服消息 - sendBigBroadcastGroup(BigBroadcastGroupMessage msg) async { - try { - final textMsg = await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createCustomMessage(data: jsonEncode(msg.toJson())); - - if (textMsg.code != 0) return; - - final sendResult = await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .sendMessage( - id: textMsg.data!.id!, - groupID: SCGlobalConfig.bigBroadcastGroup, - receiver: '', - ); - if (sendResult.code == 0) {} - } catch (e) { - throw Exception("create fail: $e"); - } - } - - Future quitGroup(String groupID) async { - await TencentImSDKPlugin.v2TIMManager.quitGroup(groupID: groupID); - } - - ///清屏 - void clearMessage() { - roomAllMsgList.clear(); - roomChatMsgList.clear(); - roomGiftMsgList.clear(); - msgChatListener?.call(Msg(groupId: "-1000", msg: "", type: "")); - msgAllListener?.call(Msg(groupId: "-1000", msg: "", type: "")); - msgGiftListener?.call(Msg(groupId: "-1000", msg: "", type: "")); - notifyListeners(); - } - - cleanRoomData() { - roomAllMsgList.clear(); - roomGiftMsgList.clear(); - roomChatMsgList.clear(); - _luckGiftPushQueue.clear(); - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; - onNewMessageListenerGroupMap.forEach((k, v) { - v = null; - }); - onNewMessageListenerGroupMap.clear(); - } - - void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { - if (SCGlobalConfig.isLuckGiftSpecialEffects) { - if (!shouldPlayLuckyGiftBurst(broadCastRes.data)) { - return; - } - final eventKey = _luckyGiftPushEventKey(broadCastRes); - if (eventKey.isNotEmpty) { - if (_currentLuckGiftPushKey == eventKey) { - return; - } - for (final queued in _luckGiftPushQueue) { - if (queued != null && _luckyGiftPushEventKey(queued) == eventKey) { - return; - } - } - } - while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) { - _luckGiftPushQueue.removeFirst(); - } - _luckGiftPushQueue.add(broadCastRes); - playLuckGiftBackCoins(); - } - } - - static bool shouldPlayLuckyGiftBurst(Data? rewardData) { - if (rewardData == null) { - return false; - } - final awardAmount = rewardData.awardAmount ?? 0; - final multiple = rewardData.multiple ?? 0; - return awardAmount > _luckyGiftBurstMinAwardAmount || - multiple >= _luckyGiftBurstMinMultiple; - } - - void cleanLuckGiftBackCoins() { - _luckGiftPushQueue.clear(); - _currentLuckGiftPushKey = null; - } - - void playLuckGiftBackCoins() { - if (currentPlayingLuckGift != null || _luckGiftPushQueue.isEmpty) { - return; - } - currentPlayingLuckGift = _luckGiftPushQueue.removeFirst(); - _currentLuckGiftPushKey = - currentPlayingLuckGift == null - ? null - : _luckyGiftPushEventKey(currentPlayingLuckGift!); - notifyListeners(); - Future.delayed(Duration(milliseconds: 3000), () { - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; - notifyListeners(); - playLuckGiftBackCoins(); - }); - } - - String _luckyGiftPushEventKey(SCBroadCastLuckGiftPush broadCastRes) { - final rewardData = broadCastRes.data; - if (rewardData == null) { - return ''; - } - return '${rewardData.roomId ?? ""}|' - '${rewardData.giftId ?? ""}|' - '${rewardData.sendUserId ?? ""}|' - '${rewardData.acceptUserId ?? ""}|' - '${rewardData.giftQuantity ?? 0}|' - '${rewardData.awardAmount ?? 0}|' - '${rewardData.multiple ?? 0}|' - '${rewardData.normalizedMultipleType}'; - } - - void updateNotificationCount(int count) { - notifcationUnReadCount = 0; - allUnReadCount = - messageUnReadCount + notifcationUnReadCount + activityUnReadCount; - notifyListeners(); - } - - void updateActivityCount(int count) { - activityUnReadCount = 0; - allUnReadCount = - messageUnReadCount + notifcationUnReadCount + activityUnReadCount; - notifyListeners(); - } - - void updateSystemCount(int count) { - for (final conversationId in SCGlobalConfig.systemConversationIds) { - conversationMap[conversationId]?.unreadCount = 0; - } - systemUnReadCount = 0; - notifyListeners(); - } - - void updateCustomerCount(int count) { - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = 0; - customerUnReadCount = 0; - notifyListeners(); - } - - void clearC2CHistoryMessage(String conversationID, bool needShowToast) async { - // 清空单聊本地及云端的消息(不删除会话) - - V2TimCallback clearC2CHistoryMessageRes = await TencentImSDKPlugin - .v2TIMManager - .getConversationManager() - .deleteConversation(conversationID: conversationID); // 需要清空记录的用户id - if (clearC2CHistoryMessageRes.code == 0) { - // 清除成功 - if (needShowToast) { - SCTts.show(SCAppLocalizations.of(context!)!.operationSuccessful); - } - initConversation(); - } else { - // 清除失败,可以查看 clearC2CHistoryMessageRes.desc 获取错误描述 - } - } -} +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_message_utils.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; +import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; +import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart' + hide Data; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/gift/gift_system_manager.dart'; + +import '../../shared/data_sources/models/enum/sc_gift_type.dart'; +import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; + +typedef RoomNewMsgListener = Function(Msg msg); +typedef OnNewMessageListener = Function(V2TimMessage? message, {String? msgId}); +typedef OnNewANMessageListener = Function(Records? message); +typedef OnRevokeMessageListener = Function(String msgId); +typedef OnNewGroupMessageListener = + Function(String groupID, V2TimMessage message); +typedef OnMessageRecvC2CReadListener = Function(List messageIDList); +typedef RtmProvider = RealTimeMessagingManager; + +class RealTimeMessagingManager extends ChangeNotifier { + static const int _giftComboMergeWindowMs = 3000; + static const int _maxLuckGiftPushQueueLength = 12; + static const int _luckyGiftFloatMinMultiple = 5; + static const int _luckyGiftBurstMinMultiple = 10; + static const int _luckyGiftBurstMinAwardAmount = 5000; + static const int _luckyGiftBurstDisplayDurationMs = 2000; + + BuildContext? context; + + void _giftFxLog(String message) { + debugPrint('[GiftFX][RTM] $message'); + } + + ///消息列表 + List roomAllMsgList = []; + List roomChatMsgList = []; + List roomGiftMsgList = []; + RoomNewMsgListener? msgAllListener; + RoomNewMsgListener? msgChatListener; + RoomNewMsgListener? msgGiftListener; + + RoomNewMsgListener? msgFloatingGiftListener; + RoomNewMsgListener? msgLuckyGiftRewardTickerListener; + RoomNewMsgListener? msgUserJoinListener; + + /// 当前会话 + V2TimConversation? currentConversation; + + /// 会话列表缓存 + Map conversationMap = {}; + + ///消息已读监听 + OnMessageRecvC2CReadListener? onMessageRecvC2CReadListener; + + ///消息被撤回监听 + OnRevokeMessageListener? onRevokeMessageListener; + + ///新消息监听 单聊 + OnNewMessageListener? onNewMessageCurrentConversationListener; + + OnNewANMessageListener? onNewActivityMessageCurrentConversationListener; + OnNewANMessageListener? onNewNotifcationMessageCurrentConversationListener; + + ///新消息监听 群聊 + Map onNewMessageListenerGroupMap = {}; + int allUnReadCount = 0; + int messageUnReadCount = 0; + int systemUnReadCount = 0; + int customerUnReadCount = 0; + int activityUnReadCount = 0; + int notifcationUnReadCount = 0; + SCBroadCastLuckGiftPush? currentPlayingLuckGift; + final Queue _luckGiftPushQueue = Queue(); + String? _currentLuckGiftPushKey; + Debouncer debouncer = Debouncer(); + List conversationList = []; + + ///客服 + SocialChatUserProfile? customerInfo; + + int _systemUnreadCount() { + int count = 0; + for (final conversationId in SCGlobalConfig.systemConversationIds) { + count += conversationMap[conversationId]?.unreadCount ?? 0; + } + return count; + } + + V2TimConversation getPreferredSystemConversation() { + final systemConversations = + conversationMap.values + .where( + (element) => + SCGlobalConfig.isSystemConversationId(element.conversationID), + ) + .toList() + ..sort((e1, e2) { + final time1 = e1.lastMessage?.timestamp ?? 0; + final time2 = e2.lastMessage?.timestamp ?? 0; + return time2.compareTo(time1); + }); + + if (systemConversations.isNotEmpty) { + return systemConversations.first; + } + + return V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: SCGlobalConfig.primarySystemUserId, + conversationID: SCGlobalConfig.primarySystemConversationId, + ); + } + + void getConversationList() { + List list = conversationMap.values.toList(); + list.removeWhere((element) { + if (element.conversationID == "c2c_${customerInfo?.id}") { + return true; + } + if (element.conversationID == "c2c_atyou-newsletter") { + ///删除这个会话,后台乱发的联系人。。防止无效未读数 + clearC2CHistoryMessage(element.conversationID, false); + return true; + } + if (element.lastMessage == null) { + return true; + } + return false; + }); + list.sort((e1, e2) { + int time1 = e1.lastMessage?.timestamp ?? 0; + int time2 = e2.lastMessage?.timestamp ?? 0; + return time2.compareTo(time1); + }); + conversationList = list; + systemUnReadCount = _systemUnreadCount(); + customerUnReadCount = + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; + + notifyListeners(); + } + + init(BuildContext context) async { + this.context = context; + V2TimSDKListener sdkListener = V2TimSDKListener( + onConnectFailed: (int code, String error) { + // 连接失败的回调函数 + // code 错误码 + // error 错误信息 + }, + onConnectSuccess: () { + // SDK 已经成功连接到腾讯云服务器 + }, + onConnecting: () { + // SDK 正在连接到腾讯云服务器 + }, + onKickedOffline: () { + // 当前用户被踢下线,此时可以 UI 提示用户,并再次调用 V2TIMManager 的 login() 函数重新登录。 + }, + onSelfInfoUpdated: (V2TimUserFullInfo info) { + // 登录用户的资料发生了更新 + // info登录用户的资料 + }, + onUserSigExpired: () { + // 在线时票据过期:此时您需要生成新的 userSig 并再次调用 V2TIMManager 的 login() 函数重新登录。 + }, + onUserStatusChanged: (List userStatusList) { + //用户状态变更通知 + //userStatusList 用户状态变化的用户列表 + //收到通知的情况:订阅过的用户发生了状态变更(包括在线状态和自定义状态),会触发该回调 + //在 IM 控制台打开了好友状态通知开关,即使未主动订阅,当好友状态发生变更时,也会触发该回调 + //同一个账号多设备登录,当其中一台设备修改了自定义状态,所有设备都会收到该回调 + }, + ); + V2TimValueCallback initSDKRes = await TencentImSDKPlugin.v2TIMManager + .initSDK( + sdkAppID: int.parse(SCGlobalConfig.tencentImAppid), // SDKAppID + loglevel: LogLevelEnum.V2TIM_LOG_ALL, // 日志登记等级 + listener: sdkListener, // 事件监听器 + ); + if (initSDKRes.code == 0) {} + try { + customerInfo = await SCConfigRepositoryImp().customerService(); + } catch (e) {} + + /// 登录 + await loginTencetRtm(context); + + /// 初始化会话列表 + // _onRefreshConversationSub = FTIM.getContactManager().addRefreshConversationListener(_onRefreshConversation); + TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .addConversationListener( + listener: V2TimConversationListener( + onNewConversation: (conversationList) { + // _onRefreshConversation(conversationList); + initConversation(); + }, + onTotalUnreadMessageCountChanged: (int totalUnreadCount) { + messageUnReadCount = totalUnreadCount; + systemUnReadCount = _systemUnreadCount(); + customerUnReadCount = + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + }, + ), + ); + TencentImSDKPlugin.v2TIMManager.addGroupListener( + listener: V2TimGroupListener( + onMemberEnter: (String groupID, List memberList) { + final rtcProvider = Provider.of( + context, + listen: false, + ); + if (groupID == + rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { + rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); + } + }, + onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { + final rtcProvider = Provider.of( + context, + listen: false, + ); + rtcProvider.removOnlineUser(groupID, member.userID!); + if (groupID == + rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { + rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); + } + }, + onMemberKicked: ( + String groupID, + V2TimGroupMemberInfo opUser, + List memberList, + ) { + ///踢出房间 + if (memberList.isNotEmpty) { + if (memberList.first.userID == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + + ///退出房间 + Provider.of( + context, + listen: false, + ).exitCurrentVoiceRoomSession(false).whenComplete(() { + SCRoomUtils.closeAllDialogs(); + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.kickRoomTips, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () {}, + ); + }, + ); + }); + } + } + }, + ), + ); + + /// 新消息监听 + // FTIM.getMessageManager().addNewMessagesListener(_onNewMessage); + TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( + listener: V2TimAdvancedMsgListener( + onRecvC2CReadReceipt: (List receiptList) { + //会话已读回调 + }, + onRecvMessageModified: (V2TimMessage message) { + // msg 为被修改之后的消息对象 + }, + onRecvMessageReadReceipts: (List receiptList) { + //群聊/单聊已读回调 + List messageIDList = []; + for (var element in receiptList) { + messageIDList.add(element.msgID!); + } + onMessageRecvC2CReadListener?.call(messageIDList); + }, + onRecvMessageRevoked: (String messageId) { + // 在本地维护的消息中处理被对方撤回的消息 + TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .deleteMessages(msgIDs: [messageId]) + .then((result) { + onRevokeMessageListener?.call(messageId); + for (var ms in conversationList) { + if (ms.lastMessage?.msgID == messageId) { + ms.lastMessage?.status = + MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED; + break; + } + } + notifyListeners(); + }); + }, + onRecvNewMessage: (V2TimMessage message) async { + _onNewMessage(message); + }, + onSendMessageProgress: (V2TimMessage message, int progress) { + //文件上传进度回调 + }, + ), + ); + getAllUnReadCount(); + joinBigBroadcastGroup(); + } + + /// 初始化会话 + /// 打开聊天界面的时候调用 + int sTime = 0; + + Future startConversation(V2TimConversation conversation) async { + assert(conversation != null); + int eTime = DateTime.now().millisecondsSinceEpoch; + if (eTime - sTime > 5000) { + sTime = eTime; + currentConversation = conversation; + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .markC2CMessageAsRead(userID: conversation.userID!); + notifyListeners(); + return true; + } else { + return false; + } + } + + ///初始话会话 + Future initConversation() async { + V2TimValueCallback convList = + await TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .getConversationList(nextSeq: '0', count: 100); + List? conversationList = + convList.data?.conversationList; + print('conversationList:${conversationList?.length}'); + conversationMap.clear(); + if (conversationList != null) { + for (V2TimConversation? conversation in conversationList) { + conversationMap[conversation!.conversationID] = conversation; + } + } + getConversationList(); + } + + // /// 所有消息未读数 + // int getAllUnReadCount() { + // allUnReadCount = 0; + // for (var value in conversationList) { + // allUnReadCount += value.unreadCount ?? 0; + // } + // // for (var value in systemConversationList) { + // // i += value.unreadMessageNum; + // // } + // notifyListeners(); + // return allUnReadCount; + // } + + /// 所有消息未读数 + void getAllUnReadCount() async { + V2TimValueCallback res = + await TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .getTotalUnreadMessageCount(); + if (res.code == 0) { + print('初始未读总数: ${res.data}'); + messageUnReadCount = res.data ?? 0; + allUnReadCount = + messageUnReadCount + activityUnReadCount + notifcationUnReadCount; + notifyListeners(); + // 这里可以先用初始值更新UI + } + } + + ///登录IM + Future loginTencetRtm(BuildContext context) async { + SocialChatLoginRes? userModel = AccountStorage().getCurrentUser(); + bool logined = false; + + while (!logined && userModel != null) { + await Future.delayed(Duration(milliseconds: 550)); + try { + if (userModel.userSig != null) { + V2TimCallback res = await TencentImSDKPlugin.v2TIMManager.login( + userID: userModel.userProfile?.id ?? "", + userSig: userModel.userSig ?? "", + ); + print( + 'tim voLogin:${res.code},${userModel.userProfile?.id},${userModel.userSig}', + ); + if (res.code == 0) { + isLogout = false; + // 登录成功逻辑 + logined = true; + print('tim 登录成功'); + await initConversation(); + } else { + // 登录失败逻辑 + //print('timm 需要重新登录2'); + print('tim 登录失败${res.code}'); + SCTts.show('tim login fail'); + } + } else { + //print('timm 需要重新登录sign'); + SCTts.show('tim login fail'); + } + } catch (e) { + //print('timm 登录异常:${e.toString()}'); + SCTts.show('timm login fail:${e.toString()}'); + } + userModel = AccountStorage().getCurrentUser(); + } + } + + _onNewMessage(V2TimMessage message) { + if (message.groupID != null) { + ///全服通知 + if (message.groupID == SCGlobalConfig.bigBroadcastGroup) { + _newBroadCastMsgRecv(message.groupID!, message); + } + + ///群消息 + for (var element in onNewMessageListenerGroupMap.values) { + element?.call(message.groupID!, message); + } + } else { + ///单聊消息 + _onNew1v1Message(message); + } + } + + _onNew1v1Message(V2TimMessage? message, {String? msgId}) async { + for (var element in conversationList) { + if (message?.userID == element?.userID) { + element.lastMessage = message; + if (onNewMessageCurrentConversationListener == null) { + element.unreadCount = element.unreadCount! + 1; + } + } + } + if (message?.userID == customerInfo?.id) { + if (onNewMessageCurrentConversationListener == null) { + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = + (conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0) + 1; + } + } + systemUnReadCount = _systemUnreadCount(); + notifyListeners(); + onNewMessageCurrentConversationListener?.call(message, msgId: msgId); + } + + void _onRefreshConversation(List conversations) { + for (V2TimConversation conversation in conversations) { + conversationMap[conversation.conversationID] = conversation; + } + getConversationList(); + } + + ///创建房间im群聊 + Future> createRoomGroup( + String groupID, + String groupName, + ) { + return V2TIMGroupManager().createGroup( + groupID: groupID, + groupType: GroupType.AVChatRoom, + groupName: groupName, + ); + } + + ///加入房间的im群聊 + Future joinRoomGroup(String groupID, String message) async { + _luckGiftPushQueue.clear(); + currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; + var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( + groupID: groupID, + message: message, + ); + if (joinResult.code == 0) { + onNewMessageListenerGroupMap[groupID] = _newGroupMsg; + } + return joinResult; + } + + ///发送文本消息 + Future dispatchMessage( + Msg msg, { + bool showEmoticons = true, + bool addLocal = true, + }) async { + if (addLocal) { + addMsg(msg); + } + // 发送消息到腾讯云IM + if (msg.groupId != null) { + await _sendTencentMessage(msg); + } + notifyListeners(); + } + + // 发送腾讯云消息 + Future _sendTencentMessage(Msg msg) async { + try { + switch (msg.type) { + case SCRoomMsgType.text: + case SCRoomMsgType.shangMai: + case SCRoomMsgType.emoticons: + case SCRoomMsgType.xiaMai: + case SCRoomMsgType.killXiaMai: + case SCRoomMsgType.roomRoleChange: + case SCRoomMsgType.roomSettingUpdate: + case SCRoomMsgType.roomBGUpdate: + case SCRoomMsgType.qcfj: + case SCRoomMsgType.fengMai: + case SCRoomMsgType.jieFeng: + case SCRoomMsgType.joinRoom: + case SCRoomMsgType.gift: + case SCRoomMsgType.bsm: + case SCRoomMsgType.roomDice: + case SCRoomMsgType.roomRPS: + case SCRoomMsgType.roomLuckNumber: + case SCRoomMsgType.image: + case SCRoomMsgType.roomGameClose: + case SCRoomMsgType.roomGameCreate: + case SCRoomMsgType.luckGiftAnimOther: + _sendRoomMessage(msg); + break; + default: + break; + } + } catch (e) { + print('发送消息失败: $e'); + // 处理发送失败的情况 + } + } + + ///发送单聊文本消息 + Future sendC2CTextMsg( + String msg, + V2TimConversation toConversation, + ) async { + // 创建文本消息 + V2TimValueCallback createTextMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createTextMessage( + text: msg, // 文本信息 + ); + if (createTextMessageRes.code == 0) { + // 文本信息创建成功 + String id = createTextMessageRes.data!.id!; + // 发送文本消息 + // 在sendMessage时,若只填写receiver则发个人用户单聊消息 + // 若只填写groupID则发群组消息 + // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 + String receiveId = toConversation.userID!; + V2TimValueCallback sendMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getMessageManager() + .sendMessage( + id: id, // 创建的messageid + receiver: toConversation.userID!, // 接收人id + needReadReceipt: true, + groupID: '', // 是否需要已读回执 + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + + ///发送单聊自定义消息 + Future sendC2CCustomMsg( + String msg, + V2TimConversation toConversation, + String extension, + ) async { + // 创建文本消息 + V2TimValueCallback createCustomMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage( + data: msg, // 文本信息 + extension: extension, + ); + if (createCustomMessageRes.code == 0) { + // 文本信息创建成功 + String id = createCustomMessageRes.data!.id!; + // 发送文本消息 + // 在sendMessage时,若只填写receiver则发个人用户单聊消息 + // 若只填写groupID则发群组消息 + // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 + String receiveId = toConversation.userID!; + V2TimValueCallback sendMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + // 创建的messageid + receiver: toConversation.userID!, + // 接收人id + needReadReceipt: true, + isSupportMessageExtension: true, + groupID: '', // 是否需要已读回执 + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + + ///发送单聊图片消息 + Future sendImageMsg({ + List? selectedList, + File? file, + required V2TimConversation conversation, + }) async { + if (file != null) { + File newFile = await SCMessageUtils.createImageElem(file); + V2TimValueCallback createImageMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createImageMessage(imagePath: newFile.path); + if (createImageMessageRes.code == 0) { + String id = createImageMessageRes.data!.id!; + V2TimValueCallback sendMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + receiver: conversation.userID!, + needReadReceipt: true, + groupID: '', + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } else { + if (selectedList != null) { + for (File entity in selectedList) { + String id = ""; + V2TimMessage? message; + //判断是视频或者图片消息 + + if (SCPathUtils.getFileType(entity.path) == "image") { + V2TimValueCallback createImageMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createImageMessage(imagePath: entity.path); + if (createImageMessageRes.code == 0) { + message = createImageMessageRes.data?.messageInfo; + id = createImageMessageRes.data!.id!; + // 创建图片 + File newFile = await SCMessageUtils.createImageElem(entity); + //发送 + V2TimImageElem elem = V2TimImageElem(path: newFile.path); + message?.imageElem = elem; + } + } else if (SCPathUtils.getFileType(entity.path) == "video_pic") { + if (entity.lengthSync() > 50000000) { + SCTts.show( + SCAppLocalizations.of(context!)!.theVideoSizeCannotExceed, + ); + return; + } + // 复制一份视频 + String md5Str1 = keyToMd5(entity.path); + File newFile = File( + "${FileCacheManager.videoCachePath}/$md5Str1.mp4", + ); + if (!newFile.existsSync()) { + await entity.copy(newFile.path); + } + // 创建缩略图 + String? thumbImagePath = await SCMessageUtils.generateFileThumbnail( + newFile.path, + 128, + ); + V2TimValueCallback createVideoMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createVideoMessage( + videoFilePath: entity.path, + type: "mp4", + duration: 0, + snapshotPath: thumbImagePath ?? "", + ); + + if (createVideoMessageRes.code == 0) { + message = createVideoMessageRes.data?.messageInfo; + id = createVideoMessageRes.data!.id!; + } + } + // 消息设置 + message?.isSelf = true; + // message.conversation = conversation; + message?.status = MessageStatus.V2TIM_MSG_STATUS_SENDING; + print('组装完成,准备发送:${message?.toJson()}'); + // String id = await FTIM.getMessageManager().sendMessage(message); + // message.msgId = id; + V2TimValueCallback sendMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + receiver: conversation.userID!, + needReadReceipt: true, + groupID: '', + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + } + notifyListeners(); + } + + // 发送消息(房间) + Future _sendRoomMessage(Msg msg) async { + try { + if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + _giftFxLog( + 'send room msg start ' + 'type=${msg.type} ' + 'groupId=${msg.groupId} ' + 'msg=${msg.msg} ' + 'giftId=${msg.gift?.id} ' + 'giftPhoto=${msg.gift?.giftPhoto}', + ); + } + if (msg.type == SCRoomMsgType.roomSettingUpdate) { + debugPrint( + "[Room Cover Sync] send roomSettingUpdate groupId=${msg.groupId ?? ""} roomId=${msg.msg ?? ""}", + ); + } + var user = msg.user?.copyWith(); + var toUser = msg.toUser?.copyWith(); + user?.cleanWearHonor(); + user?.cleanWearBadge(); + user?.cleanPhotos(); + toUser?.cleanWearHonor(); + toUser?.cleanWearBadge(); + toUser?.cleanUseProps(); + toUser?.cleanPhotos(); + msg.needUpDataUserInfo = + Provider.of( + context!, + listen: false, + ).needUpDataUserInfo; + msg.user = user; + msg.toUser = toUser; + final textMsg = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage(data: jsonEncode(msg.toJson())); + + if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + _giftFxLog( + 'send room msg createCustomMessage ' + 'type=${msg.type} ' + 'code=${textMsg.code} ' + 'id=${textMsg.data?.id} ' + 'desc=${textMsg.desc}', + ); + } + + if (textMsg.code != 0) return; + + final sendResult = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: textMsg.data!.id!, + groupID: msg.groupId!, + receiver: '', + ); + if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + _giftFxLog( + 'send room msg result ' + 'type=${msg.type} ' + 'code=${sendResult.code} ' + 'msgId=${sendResult.data?.msgID} ' + 'groupId=${sendResult.data?.groupID} ' + 'desc=${sendResult.desc}', + ); + } + if (sendResult.code == 0) {} + } catch (e) { + throw Exception("create fail: $e"); + } + } + + /// 添加消息 + addMsg(Msg msg) { + final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg); + if (mergedGiftMsg != null) { + msgAllListener?.call(mergedGiftMsg); + msgGiftListener?.call(mergedGiftMsg); + if (msg.type == SCRoomMsgType.gift) { + msgFloatingGiftListener?.call(msg); + } + notifyListeners(); + return; + } + + roomAllMsgList.insert(0, msg); + if (roomAllMsgList.length > 250) { + print('大于200条消息'); + roomAllMsgList.removeAt(roomAllMsgList.length - 1); + } + msgAllListener?.call(msg); + + if (msg.type == SCRoomMsgType.text) { + roomChatMsgList.insert(0, msg); + if (roomChatMsgList.length > 250) { + print('大于200条消息'); + roomChatMsgList.removeAt(roomChatMsgList.length - 1); + } + msgChatListener?.call(msg); + } else if (msg.type == SCRoomMsgType.image) { + roomChatMsgList.insert(0, msg); + if (roomChatMsgList.length > 250) { + print('大于200条消息'); + roomChatMsgList.removeAt(roomChatMsgList.length - 1); + } + msgChatListener?.call(msg); + } else if (msg.type == SCRoomMsgType.gift || + msg.type == SCRoomMsgType.luckGiftAnimOther) { + roomGiftMsgList.insert(0, msg); + if (roomGiftMsgList.length > 250) { + print('大于200条消息'); + roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); + } + msgGiftListener?.call(msg); + if (msg.type == SCRoomMsgType.gift) { + msgFloatingGiftListener?.call(msg); + } + } + } + + Msg? _mergeGiftMessageIfNeeded(Msg incoming) { + if (incoming.type != SCRoomMsgType.gift && + incoming.type != SCRoomMsgType.luckGiftAnimOther) { + return null; + } + + final mergeTarget = _findMergeableGiftMessage(incoming); + if (mergeTarget == null) { + return null; + } + + mergeTarget.number = (mergeTarget.number ?? 0) + (incoming.number ?? 0); + mergeTarget.time = DateTime.now().millisecondsSinceEpoch; + if ((incoming.msg ?? "").trim().isNotEmpty) { + mergeTarget.msg = incoming.msg; + } + + _moveMessageToFront(roomGiftMsgList, mergeTarget); + _moveMessageToFront(roomAllMsgList, mergeTarget); + return mergeTarget; + } + + Msg? _findMergeableGiftMessage(Msg incoming) { + final now = DateTime.now().millisecondsSinceEpoch; + for (final existing in roomGiftMsgList) { + if ((existing.time ?? 0) <= 0 || + now - (existing.time ?? 0) > _giftComboMergeWindowMs) { + continue; + } + if (_isSameGiftComboMessage(existing, incoming)) { + return existing; + } + } + return null; + } + + bool _isSameGiftComboMessage(Msg existing, Msg incoming) { + return existing.type == incoming.type && + existing.groupId == incoming.groupId && + existing.user?.id == incoming.user?.id && + existing.toUser?.id == incoming.toUser?.id && + existing.gift?.id == incoming.gift?.id; + } + + void _moveMessageToFront(List messages, Msg target) { + final index = messages.indexOf(target); + if (index <= 0) { + return; + } + messages.removeAt(index); + messages.insert(0, target); + } + + bool _shouldHighlightLuckyGiftReward(SCBroadCastLuckGiftPush broadCastRes) { + final rewardData = broadCastRes.data; + if (rewardData == null) { + return false; + } + if (rewardData.isBigReward) { + return true; + } + return (rewardData.multiple ?? 0) >= 5; + } + + bool _isLuckyGiftInCurrentRoom(SCBroadCastLuckGiftPush broadCastRes) { + if (context == null) { + return false; + } + final currentRoomId = + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + ''; + final roomId = broadCastRes.data?.roomId ?? ''; + return currentRoomId.isNotEmpty && + roomId.isNotEmpty && + currentRoomId == roomId; + } + + SCFloatingMessage _buildLuckyGiftFloatingMessage( + SCBroadCastLuckGiftPush broadCastRes, + ) { + final rewardData = broadCastRes.data; + final resolvedGiftUrl = _resolveLuckyGiftGiftPhoto(rewardData); + return SCFloatingMessage( + type: 0, + userId: rewardData?.sendUserId, + roomId: rewardData?.roomId, + toUserId: rewardData?.acceptUserId, + userAvatarUrl: rewardData?.userAvatar, + userName: rewardData?.nickname, + toUserName: rewardData?.acceptNickname, + giftUrl: resolvedGiftUrl, + giftId: rewardData?.giftId, + number: rewardData?.giftQuantity, + coins: rewardData?.awardAmount, + multiple: rewardData?.multiple, + priority: 1000, + ); + } + + void _handleLuckyGiftGlobalNews( + SCBroadCastLuckGiftPush broadCastRes, { + required String source, + }) { + final rewardData = broadCastRes.data; + if (rewardData == null) { + return; + } + if (_isLuckyGiftInCurrentRoom(broadCastRes)) { + addluckGiftPushQueue(broadCastRes); + } + if (!rewardData.shouldShowGlobalNews || + (rewardData.multiple ?? 0) < _luckyGiftFloatMinMultiple) { + return; + } + if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) { + _giftFxLog( + 'skip global lucky gift overlay ' + 'reason=current_room_already_receives_group_msg ' + 'roomId=${rewardData.roomId} ' + 'giftId=${rewardData.giftId}', + ); + return; + } + OverlayManager().addMessage(_buildLuckyGiftFloatingMessage(broadCastRes)); + } + + void _handleRoomLuckyGiftMessage(SCBroadCastLuckGiftPush broadCastRes) { + final rewardData = broadCastRes.data; + if (rewardData == null) { + return; + } + final resolvedGiftUrl = _resolveLuckyGiftGiftPhoto(rewardData); + final roomMsg = Msg( + groupId: '', + msg: '', + type: SCRoomMsgType.gameLuckyGift, + ); + roomMsg.gift = SocialChatGiftRes( + id: rewardData.giftId, + giftPhoto: resolvedGiftUrl, + giftTab: 'LUCK', + ); + roomMsg.number = 0; + roomMsg.awardAmount = rewardData.awardAmount; + roomMsg.user = SocialChatUserProfile( + id: rewardData.sendUserId, + userNickname: rewardData.nickname, + userAvatar: rewardData.userAvatar, + ); + roomMsg.toUser = SocialChatUserProfile( + id: rewardData.acceptUserId, + userNickname: rewardData.acceptNickname, + ); + addMsg(roomMsg); + msgLuckyGiftRewardTickerListener?.call(roomMsg); + + if (_shouldHighlightLuckyGiftReward(broadCastRes)) { + final highlightMsg = Msg( + groupId: '', + msg: '${rewardData.multiple ?? 0}', + type: SCRoomMsgType.gameLuckyGift_5, + ); + highlightMsg.awardAmount = rewardData.awardAmount; + highlightMsg.gift = SocialChatGiftRes( + id: rewardData.giftId, + giftPhoto: resolvedGiftUrl, + giftTab: 'LUCK', + ); + highlightMsg.user = SocialChatUserProfile( + id: rewardData.sendUserId, + userNickname: rewardData.nickname, + ); + addMsg(highlightMsg); + } + + addluckGiftPushQueue(broadCastRes); + _handleLuckyGiftGlobalNews(broadCastRes, source: 'room_group'); + + if (rewardData.sendUserId == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).updateLuckyRewardAmount(roomMsg.awardAmount ?? 0); + } + } + + String _resolveLuckyGiftGiftPhoto(Data? rewardData) { + final fallbackUrl = (rewardData?.giftCover ?? '').trim(); + final giftId = (rewardData?.giftId ?? '').trim(); + final currentContext = context; + if (giftId.isEmpty || currentContext == null || !currentContext.mounted) { + _giftFxLog( + 'resolve lucky float gift photo skipped ' + 'giftId=$giftId ' + 'fallbackUrl=$fallbackUrl ' + 'reason=${giftId.isEmpty ? "empty_gift_id" : "invalid_context"}', + ); + return fallbackUrl; + } + final appGeneralManager = Provider.of( + currentContext, + listen: false, + ); + final giftById = appGeneralManager.getGiftById(giftId); + final giftByStandardId = appGeneralManager.getGiftByStandardId(giftId); + final localGift = giftById ?? giftByStandardId; + final localGiftPhoto = (localGift?.giftPhoto ?? '').trim(); + _giftFxLog( + 'resolve lucky float gift photo ' + 'giftId=$giftId ' + 'fallbackUrl=$fallbackUrl ' + 'giftById.id=${giftById?.id} ' + 'giftById.standardId=${giftById?.standardId} ' + 'giftById.photo=${giftById?.giftPhoto} ' + 'giftByStandardId.id=${giftByStandardId?.id} ' + 'giftByStandardId.standardId=${giftByStandardId?.standardId} ' + 'giftByStandardId.photo=${giftByStandardId?.giftPhoto} ' + 'resolvedUrl=${localGiftPhoto.isNotEmpty ? localGiftPhoto : fallbackUrl}', + ); + if (localGiftPhoto.isNotEmpty) { + return localGiftPhoto; + } + unawaited(appGeneralManager.giftList()); + return fallbackUrl; + } + + bool isLogout = false; + + logout() async { + V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout(); + TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .removeAdvancedMsgListener(); + if (logoutRes.code == 0) { + isLogout = true; + } + } + + ///全服广播消息 + _newBroadCastMsgRecv(String groupID, V2TimMessage message) async { + try { + String? customData = message.customElem?.data; + if (customData != null && customData.isNotEmpty) { + final data = json.decode(customData); + var type = data["type"]; + if (type == "SYS_ACTIVITY") { + if (onNewActivityMessageCurrentConversationListener != null) { + var recode = Records.fromJson(data["data"]); + onNewActivityMessageCurrentConversationListener?.call(recode); + } else { + activityUnReadCount = activityUnReadCount + 1; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + } + } else if (type == "SYS_ANNOUNCEMENT") { + if (onNewNotifcationMessageCurrentConversationListener != null) { + var recode = Records.fromJson(data["data"]); + onNewNotifcationMessageCurrentConversationListener?.call(recode); + } else { + notifcationUnReadCount = notifcationUnReadCount + 1; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + } + } else if (type == "GAME_BAISHUN_WIN") { + if (SCGlobalConfig.isReview) { + ///审核状态不播放动画 + return; + } + var fdata = data["data"]; + var winCoins = fdata["currencyDiff"]; + if (winCoins > 14999) { + ///达到5000才飘屏 + SCFloatingMessage msg = SCFloatingMessage( + type: 2, + userId: fdata["account"], + userAvatarUrl: fdata["userAvatar"], + userName: fdata["userNickname"], + giftUrl: fdata["gameUrl"], + roomId: fdata["roomId"], + coins: fdata["currencyDiff"], + ); + OverlayManager().addMessage(msg); + } + } else if (type == "GAME_LUCKY_GIFT") { + final broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); + _giftFxLog( + 'recv GAME_LUCKY_GIFT broadcast ' + 'giftId=${broadCastRes.data?.giftId} ' + 'roomId=${broadCastRes.data?.roomId} ' + 'sendUserId=${broadCastRes.data?.sendUserId} ' + 'acceptUserId=${broadCastRes.data?.acceptUserId} ' + 'giftQuantity=${broadCastRes.data?.giftQuantity} ' + 'awardAmount=${broadCastRes.data?.awardAmount} ' + 'multiple=${broadCastRes.data?.multiple} ' + 'multipleType=${broadCastRes.data?.multipleType} ' + 'globalNews=${broadCastRes.data?.globalNews}', + ); + _handleLuckyGiftGlobalNews(broadCastRes, source: 'broadcast'); + } else if (type == "REGISTER_REWARD_GRANTED") { + await DataPersistence.setPendingRegisterRewardDialog(true); + await DataPersistence.clearAwaitRegisterRewardSocket(); + eventBus.fire(RegisterRewardGrantedEvent(data: data["data"])); + } else if (type == "ROCKET_ENERGY_LAUNCH") { + ///火箭触发飘屏 + var fdata = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 3, + roomId: fdata["roomId"], + rocketLevel: fdata["fromLevel"], + userAvatarUrl: fdata["userAvatar"], + userName: fdata["nickname"], + userId: fdata["actualAccount"], + priority: 1000, + ); + OverlayManager().addMessage(msg); + } else if (type == SCRoomMsgType.roomRedPacket) { + ///红包触发飘屏 + var fData = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 4, + roomId: fData["roomId"], + userAvatarUrl: fData["userAvatar"], + userName: fData["userNickname"], + userId: fData["actualAccount"], + toUserId: fData["packetId"], + priority: 1000, + ); + if (msg.roomId == + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id) { + Provider.of( + context!, + listen: false, + ).loadRoomRedPacketList(1); + } + OverlayManager().addMessage(msg); + } else if (type == SCRoomMsgType.inviteRoom) { + ///邀请进入房间 + var fdata = data["data"]; + SCFloatingMessage msg = SCFloatingMessage.fromJson(fdata); + if (msg.toUserId == + AccountStorage().getCurrentUser()?.userProfile?.id && + msg.roomId != + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id) { + SmartDialog.dismiss(tag: "showInviteRoom"); + SmartDialog.show( + tag: "showInviteRoom", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return InviteRoomDialog(msg); + }, + ); + } + } + } + } catch (e) {} + } + + _newGroupMsg(String groupID, V2TimMessage message) { + if (groupID != + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.roomAccount) { + return; + } + try { + String? customData = message.customElem?.data; + if (customData != null && customData.isNotEmpty) { + // 直接处理字符串格式的自定义数据 + final data = json.decode(customData); + debugPrint(">>>>>>>>>>>>>>>>>>>消息类型${data["type"]}"); + + if (data["type"] == SCRoomMsgType.roomRedPacket) { + ///房间红包 + var fData = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 4, + roomId: fData["roomId"], + userAvatarUrl: fData["userAvatar"], + userName: fData["userNickname"], + userId: fData["actualAccount"], + toUserId: fData["packetId"], + priority: 1000, + ); + Provider.of( + context!, + listen: false, + ).loadRoomRedPacketList(1); + OverlayManager().addMessage(msg); + return; + } + Msg msg = Msg.fromJson(data); + + if (msg.type == SCRoomMsgType.sendGift || + msg.type == SCRoomMsgType.gameBurstCrystalSprint || + msg.type == SCRoomMsgType.gameBurstCrystalBox) { + ///这个消息暂时不监听 + return; + } + if (msg.type == SCRoomMsgType.bsm) { + if (msg.toUser?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context!)!.tips, + msg: SCAppLocalizations.of( + context!, + )!.invitesYouToTheMicrophone(msg.msg ?? ""), + btnText: SCAppLocalizations.of(context!)!.confirm, + onEnsure: () { + ///上麦 + num index = + Provider.of( + context!, + listen: false, + ).findWheat(); + if (index > -1) { + Provider.of( + context!, + listen: false, + ).shangMai( + index, + eventType: "INVITE", + inviterId: msg.role, + ); + } + }, + ); + }, + ); + } + return; + } + if (msg.type == SCRoomMsgType.killXiaMai) { + ///踢下麦 + if (msg.msg == AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + } + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(notifyIfUnchanged: false); + return; + } + + if (msg.type == SCRoomMsgType.roomSettingUpdate) { + debugPrint( + "[Room Cover Sync] recv roomSettingUpdate groupId=$groupID roomId=${msg.msg ?? ""}", + ); + Provider.of( + context!, + listen: false, + ).loadRoomInfo(msg.msg ?? ""); + return; + } + if (msg.type == SCRoomMsgType.roomBGUpdate) { + SCRoomThemeListRes res; + if ((msg.msg ?? "").isNotEmpty) { + res = SCRoomThemeListRes.fromJson(jsonDecode(msg.msg!)); + } else { + res = SCRoomThemeListRes(); + } + Provider.of( + context!, + listen: false, + ).updateRoomBG(res); + return; + } + if (msg.type == SCRoomMsgType.emoticons) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + return; + } + if (msg.type == SCRoomMsgType.micChange) { + Provider.of( + context!, + listen: false, + ).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); + } else if (msg.type == SCRoomMsgType.shangMai || + msg.type == SCRoomMsgType.xiaMai || + msg.type == SCRoomMsgType.fengMai || + msg.type == SCRoomMsgType.jieFeng) { + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(notifyIfUnchanged: false); + } else if (msg.type == SCRoomMsgType.refreshOnlineUser) { + Provider.of( + context!, + listen: false, + ).fetchOnlineUsersList(notifyIfUnchanged: false); + } else if (msg.type == SCRoomMsgType.gameLuckyGift) { + var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); + _giftFxLog( + 'recv GAME_LUCKY_GIFT ' + 'giftId=${broadCastRes.data?.giftId} ' + 'roomId=${broadCastRes.data?.roomId} ' + 'sendUserId=${broadCastRes.data?.sendUserId} ' + 'acceptUserId=${broadCastRes.data?.acceptUserId} ' + 'giftQuantity=${broadCastRes.data?.giftQuantity} ' + 'awardAmount=${broadCastRes.data?.awardAmount} ' + 'multiple=${broadCastRes.data?.multiple} ' + 'multipleType=${broadCastRes.data?.multipleType} ' + 'globalNews=${broadCastRes.data?.globalNews}', + ); + _handleRoomLuckyGiftMessage(broadCastRes); + } else { + if (msg.type == SCRoomMsgType.joinRoom) { + final shouldShowRoomVisualEffects = + Provider.of( + context!, + listen: false, + ).shouldShowRoomVisualEffects; + if (msg.user != null) { + Provider.of( + context!, + listen: false, + ).addOnlineUser(msg.groupId ?? "", msg.user!); + } + if (msgUserJoinListener != null) { + msgUserJoinListener!(msg); + } + + ///坐骑 + if (msg.user?.getMountains() != null) { + if (SCGlobalConfig.isEntryVehicleAnimation && + shouldShowRoomVisualEffects) { + SCGiftVapSvgaManager().play( + msg.user?.getMountains()?.sourceUrl ?? "", + priority: 100, + type: 1, + ); + } + } + } else if (msg.type == SCRoomMsgType.gift) { + final gift = msg.gift; + if (gift == null) { + _giftFxLog( + 'recv gift msg skipped reason=no_gift ' + 'fromUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'quantity=${msg.number}', + ); + } else { + final rtcProvider = Provider.of( + context!, + listen: false, + ); + final special = gift.special ?? ""; + final giftSourceUrl = gift.giftSourceUrl ?? ""; + final hasSource = giftSourceUrl.isNotEmpty; + final hasAnimation = scGiftHasAnimationSpecial(special); + final hasGlobalGift = special.contains( + SCGiftType.GLOBAL_GIFT.name, + ); + final hasFullScreenEffect = scGiftHasFullScreenEffect(special); + _giftFxLog( + 'recv gift msg ' + 'fromUserId=${msg.user?.id} ' + 'fromUserName=${msg.user?.userNickname} ' + 'toUserId=${msg.toUser?.id} ' + 'toUserName=${msg.toUser?.userNickname} ' + 'giftId=${gift.id} ' + 'giftName=${gift.giftName} ' + 'giftSourceUrl=$giftSourceUrl ' + 'special=$special ' + 'hasSource=$hasSource ' + 'hasAnimation=$hasAnimation ' + 'hasGlobalGift=$hasGlobalGift ' + 'hasFullScreenEffect=$hasFullScreenEffect ' + 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', + ); + if (giftSourceUrl.isNotEmpty && special.isNotEmpty) { + if (scGiftHasFullScreenEffect(special)) { + if (SCGlobalConfig.isGiftSpecialEffects && + rtcProvider.shouldShowRoomVisualEffects) { + _giftFxLog( + 'trigger player play path=$giftSourceUrl ' + 'giftId=${gift.id} giftName=${gift.giftName}', + ); + SCGiftVapSvgaManager().play(giftSourceUrl); + } else { + _giftFxLog( + 'skip player play because visual effects disabled ' + 'giftId=${gift.id} ' + 'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} ' + 'roomVisible=${rtcProvider.shouldShowRoomVisualEffects}', + ); + } + } else { + _giftFxLog( + 'skip player play because special does not include ' + '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' + 'giftId=${gift.id} special=${gift.special}', + ); + } + } else { + _giftFxLog( + 'skip player play because giftSourceUrl or special is empty ' + 'giftId=${gift.id} ' + 'giftSourceUrl=${gift.giftSourceUrl} ' + 'special=${gift.special}', + ); + } + if (rtcProvider + .currenRoom + ?.roomProfile + ?.roomSetting + ?.showHeartbeat ?? + false) { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + rtcProvider.requestGiftTriggeredMicRefresh(); + }, + ); + } + final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0); + if (coins > 9999) { + OverlayManager().addMessage( + SCFloatingMessage( + type: 1, + userAvatarUrl: msg.user?.userAvatar ?? "", + userName: msg.user?.userNickname ?? "", + toUserName: msg.toUser?.userNickname ?? "", + toUserAvatarUrl: msg.toUser?.userAvatar ?? "", + giftUrl: gift.giftPhoto, + number: msg.number, + coins: coins, + roomId: msg.msg, + ), + ); + } + } + } else if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + final hideLGiftAnimal = + Provider.of( + context!, + listen: false, + ).hideLGiftAnimal; + if (hideLGiftAnimal) { + _giftFxLog( + 'recv LUCK_GIFT_ANIM_OTHER skipped ' + 'reason=hideLGiftAnimal ' + 'giftPhoto=${msg.gift?.giftPhoto}', + ); + } else { + final targetUserIds = + (jsonDecode(msg.msg ?? "") as List) + .map((e) => e as String) + .toList(); + _giftFxLog( + 'recv LUCK_GIFT_ANIM_OTHER ' + 'giftPhoto=${msg.gift?.giftPhoto} ' + 'sendUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'quantity=${msg.number} ' + 'targetUserIds=${targetUserIds.join(",")}', + ); + eventBus.fire( + GiveRoomLuckWithOtherEvent( + msg.gift?.giftPhoto ?? "", + targetUserIds, + ), + ); + if (msg.user != null && msg.toUser != null && msg.gift != null) { + _giftFxLog( + 'trigger floating gift listener from LUCK_GIFT_ANIM_OTHER ' + 'sendUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'quantity=${msg.number} ' + 'giftId=${msg.gift?.id}', + ); + msgFloatingGiftListener?.call(msg); + } else { + _giftFxLog( + 'skip floating gift listener from LUCK_GIFT_ANIM_OTHER ' + 'reason=incomplete_msg ' + 'sendUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'giftId=${msg.gift?.id} ' + 'quantity=${msg.number}', + ); + } + } + } else if (msg.type == SCRoomMsgType.roomRoleChange) { + ///房间身份变动 + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(); + if (msg.toUser?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).currenRoom?.entrants?.setRoles(msg.msg); + if (msg.msg == SCRoomRolesType.TOURIST.name && + !(Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomSetting?.touristMike ?? + false)) { + ///如果变成了游客,房间又是禁止游客上麦,需要下麦 + num index = Provider.of( + context!, + listen: false, + ).userOnMaiInIndex( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); + if (index > -1) { + Provider.of( + context!, + listen: false, + ).xiaMai(index); + } + } + } + } else if (msg.type == SCRoomMsgType.roomDice) { + if ((msg.number ?? -1) > -1) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + } + } else if (msg.type == SCRoomMsgType.roomRPS) { + if ((msg.number ?? -1) > -1) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + } + } + addMsg(msg); + } + } + } catch (e) { + throw Exception("message parser fail: $e"); + } + } + + ///加入全服广播群 + joinBigBroadcastGroup() async { + bool joined = false; + while (!isLogout && !joined) { + await Future.delayed(Duration(milliseconds: 550)); + try { + var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( + groupID: SCGlobalConfig.bigBroadcastGroup, + message: "", + ); + if (joinResult.code == 0) { + joined = true; + } + } catch (e) { + //print('timm 登录异常:${e.toString()}'); + SCTts.show('broadcastGroup join fail:${e.toString()}'); + } + } + } + + ///发送全服消息 + sendBigBroadcastGroup(BigBroadcastGroupMessage msg) async { + try { + final textMsg = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage(data: jsonEncode(msg.toJson())); + + if (textMsg.code != 0) return; + + final sendResult = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: textMsg.data!.id!, + groupID: SCGlobalConfig.bigBroadcastGroup, + receiver: '', + ); + if (sendResult.code == 0) {} + } catch (e) { + throw Exception("create fail: $e"); + } + } + + Future quitGroup(String groupID) async { + await TencentImSDKPlugin.v2TIMManager.quitGroup(groupID: groupID); + } + + ///清屏 + void clearMessage() { + roomAllMsgList.clear(); + roomChatMsgList.clear(); + roomGiftMsgList.clear(); + msgChatListener?.call(Msg(groupId: "-1000", msg: "", type: "")); + msgAllListener?.call(Msg(groupId: "-1000", msg: "", type: "")); + msgGiftListener?.call(Msg(groupId: "-1000", msg: "", type: "")); + notifyListeners(); + } + + cleanRoomData() { + roomAllMsgList.clear(); + roomGiftMsgList.clear(); + roomChatMsgList.clear(); + _luckGiftPushQueue.clear(); + currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; + onNewMessageListenerGroupMap.forEach((k, v) { + v = null; + }); + onNewMessageListenerGroupMap.clear(); + } + + void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { + if (SCGlobalConfig.isLuckGiftSpecialEffects) { + if (!shouldPlayLuckyGiftBurst(broadCastRes.data)) { + return; + } + final eventKey = _luckyGiftPushEventKey(broadCastRes); + if (eventKey.isNotEmpty) { + if (_currentLuckGiftPushKey == eventKey) { + return; + } + for (final queued in _luckGiftPushQueue) { + if (queued != null && _luckyGiftPushEventKey(queued) == eventKey) { + return; + } + } + } + while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) { + _luckGiftPushQueue.removeFirst(); + } + _luckGiftPushQueue.add(broadCastRes); + playLuckGiftBackCoins(); + } + } + + static bool shouldPlayLuckyGiftBurst(Data? rewardData) { + if (rewardData == null) { + return false; + } + final awardAmount = rewardData.awardAmount ?? 0; + final multiple = rewardData.multiple ?? 0; + return awardAmount > _luckyGiftBurstMinAwardAmount || + multiple >= _luckyGiftBurstMinMultiple; + } + + void cleanLuckGiftBackCoins() { + _luckGiftPushQueue.clear(); + _currentLuckGiftPushKey = null; + } + + void playLuckGiftBackCoins() { + if (currentPlayingLuckGift != null || _luckGiftPushQueue.isEmpty) { + return; + } + currentPlayingLuckGift = _luckGiftPushQueue.removeFirst(); + _currentLuckGiftPushKey = + currentPlayingLuckGift == null + ? null + : _luckyGiftPushEventKey(currentPlayingLuckGift!); + notifyListeners(); + Future.delayed( + Duration(milliseconds: _luckyGiftBurstDisplayDurationMs), + () { + currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; + notifyListeners(); + playLuckGiftBackCoins(); + }, + ); + } + + String _luckyGiftPushEventKey(SCBroadCastLuckGiftPush broadCastRes) { + final rewardData = broadCastRes.data; + if (rewardData == null) { + return ''; + } + return '${rewardData.roomId ?? ""}|' + '${rewardData.giftId ?? ""}|' + '${rewardData.sendUserId ?? ""}|' + '${rewardData.acceptUserId ?? ""}|' + '${rewardData.giftQuantity ?? 0}|' + '${rewardData.awardAmount ?? 0}|' + '${rewardData.multiple ?? 0}|' + '${rewardData.normalizedMultipleType}'; + } + + void updateNotificationCount(int count) { + notifcationUnReadCount = 0; + allUnReadCount = + messageUnReadCount + notifcationUnReadCount + activityUnReadCount; + notifyListeners(); + } + + void updateActivityCount(int count) { + activityUnReadCount = 0; + allUnReadCount = + messageUnReadCount + notifcationUnReadCount + activityUnReadCount; + notifyListeners(); + } + + void updateSystemCount(int count) { + for (final conversationId in SCGlobalConfig.systemConversationIds) { + conversationMap[conversationId]?.unreadCount = 0; + } + systemUnReadCount = 0; + notifyListeners(); + } + + void updateCustomerCount(int count) { + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = 0; + customerUnReadCount = 0; + notifyListeners(); + } + + void clearC2CHistoryMessage(String conversationID, bool needShowToast) async { + // 清空单聊本地及云端的消息(不删除会话) + + V2TimCallback clearC2CHistoryMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getConversationManager() + .deleteConversation(conversationID: conversationID); // 需要清空记录的用户id + if (clearC2CHistoryMessageRes.code == 0) { + // 清除成功 + if (needShowToast) { + SCTts.show(SCAppLocalizations.of(context!)!.operationSuccessful); + } + initConversation(); + } else { + // 清除失败,可以查看 clearC2CHistoryMessageRes.desc 获取错误描述 + } + } +} diff --git a/lib/services/general/sc_app_general_manager.dart b/lib/services/general/sc_app_general_manager.dart index 1b3ee6c..e229272 100644 --- a/lib/services/general/sc_app_general_manager.dart +++ b/lib/services/general/sc_app_general_manager.dart @@ -27,6 +27,7 @@ class SCAppGeneralManager extends ChangeNotifier { ///礼物 List giftResList = []; final Map _giftByIdMap = {}; + final Map _giftByStandardIdMap = {}; final Set _warmedGiftCoverUrls = {}; final Set _warmingGiftCoverUrls = {}; bool _isFetchingGiftList = false; @@ -169,6 +170,7 @@ class SCAppGeneralManager extends ChangeNotifier { void _rebuildGiftTabs({required bool includeCustomized}) { giftByTab.clear(); _giftByIdMap.clear(); + _giftByStandardIdMap.clear(); for (var gift in giftResList) { giftByTab[gift.giftTab]; var gmap = giftByTab[gift.giftTab]; @@ -187,6 +189,10 @@ class SCAppGeneralManager extends ChangeNotifier { } giftByTab["ALL"] = gAllMap; _giftByIdMap[gift.id!] = gift; + final standardId = (gift.standardId ?? "").trim(); + if (standardId.isNotEmpty && standardId != "0") { + _giftByStandardIdMap[standardId] = gift; + } } if (includeCustomized) { @@ -277,6 +283,18 @@ class SCAppGeneralManager extends ChangeNotifier { return _giftByIdMap[id]; } + SocialChatGiftRes? getGiftByStandardId(String standardId) { + return _giftByStandardIdMap[standardId]; + } + + SocialChatGiftRes? getGiftByIdOrStandardId(String id) { + final normalizedId = id.trim(); + if (normalizedId.isEmpty) { + return null; + } + return _giftByIdMap[normalizedId] ?? _giftByStandardIdMap[normalizedId]; + } + void downLoad(List giftResList) { final scheduledPaths = {}; var scheduledCount = 0; diff --git a/lib/services/gift/gift_animation_manager.dart b/lib/services/gift/gift_animation_manager.dart index 8b93f74..576ea2a 100644 --- a/lib/services/gift/gift_animation_manager.dart +++ b/lib/services/gift/gift_animation_manager.dart @@ -1,140 +1,279 @@ -import 'dart:collection'; - -import 'package:flutter/cupertino.dart'; - -import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; - -class GiftAnimationManager extends ChangeNotifier { - static const int _maxPendingAnimations = 24; - - Queue pendingAnimationsQueue = Queue(); - List animationControllerList = []; - - //每个控件正在播放的动画 - Map giftMap = {0: null, 1: null, 2: null, 3: null}; - - bool get _controllersReady => - animationControllerList.length >= giftMap.length; - - void enqueueGiftAnimation(LGiftModel giftModel) { - if (_mergeIntoActiveAnimation(giftModel)) { - return; - } - if (_mergeIntoPendingAnimation(giftModel)) { - return; - } - _trimPendingAnimations(); - pendingAnimationsQueue.add(giftModel); - proceedToNextAnimation(); - } - - void _trimPendingAnimations() { - while (pendingAnimationsQueue.length >= _maxPendingAnimations) { - pendingAnimationsQueue.removeFirst(); - } - } - - bool _mergeIntoActiveAnimation(LGiftModel incoming) { - for (final entry in giftMap.entries) { - final current = entry.value; - if (current == null || current.labelId != incoming.labelId) { - continue; - } - _mergeGiftModel(target: current, incoming: incoming); - notifyListeners(); - if (animationControllerList.length > entry.key) { - animationControllerList[entry.key].controller.forward(from: 0.45); - } - return true; - } - return false; - } - - bool _mergeIntoPendingAnimation(LGiftModel incoming) { - for (final pending in pendingAnimationsQueue) { - if (pending.labelId != incoming.labelId) { - continue; - } - _mergeGiftModel(target: pending, incoming: incoming); - return true; - } - return false; - } - - void _mergeGiftModel({ - required LGiftModel target, - required LGiftModel incoming, - }) { - target.giftCount = (target.giftCount) + (incoming.giftCount); - if (target.sendUserName.isEmpty) { - target.sendUserName = incoming.sendUserName; - } - if (target.sendToUserName.isEmpty) { - target.sendToUserName = incoming.sendToUserName; - } - if (target.sendUserPic.isEmpty) { - target.sendUserPic = incoming.sendUserPic; - } - if (target.giftPic.isEmpty) { - target.giftPic = incoming.giftPic; - } - if (target.giftName.isEmpty) { - target.giftName = incoming.giftName; - } - target.rewardAmount = target.rewardAmount + incoming.rewardAmount; - if (incoming.rewardAmountText.isNotEmpty) { - target.rewardAmountText = incoming.rewardAmountText; - } - target.showLuckyRewardFrame = - target.showLuckyRewardFrame || incoming.showLuckyRewardFrame; - } - - ///开始播放 - proceedToNextAnimation() { - if (pendingAnimationsQueue.isEmpty || !_controllersReady) { - return; - } - var playGift = pendingAnimationsQueue.first; - for (var key in giftMap.keys) { - var value = giftMap[key]; - if (value == null) { - giftMap[key] = playGift; - pendingAnimationsQueue.removeFirst(); - notifyListeners(); - animationControllerList[key].controller.forward(from: 0); - break; - } else { - if (value.labelId == playGift.labelId) { - _mergeGiftModel(target: value, incoming: playGift); - pendingAnimationsQueue.removeFirst(); - notifyListeners(); - animationControllerList[key].controller.forward(from: 0.45); - break; - } - } - } - } - - void attachAnimationControllers(List anins) { - animationControllerList = anins; - proceedToNextAnimation(); - } - - void cleanupAnimationResources() { - pendingAnimationsQueue.clear(); - giftMap[0] = null; - giftMap[1] = null; - giftMap[2] = null; - giftMap[3] = null; - for (var element in animationControllerList) { - element.controller.dispose(); - } - animationControllerList.clear(); - } - - void markAnimationAsFinished(int index) { - giftMap[index] = null; - notifyListeners(); - proceedToNextAnimation(); - } -} +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/cupertino.dart'; + +import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; + +class GiftAnimationManager extends ChangeNotifier { + static const int _maxPendingAnimations = 24; + static const Duration _activeComboIdleDismissDelay = Duration( + milliseconds: 3200, + ); + + Queue pendingAnimationsQueue = Queue(); + List animationControllerList = []; + + //每个控件正在播放的动画 + Map giftMap = {0: null, 1: null, 2: null, 3: null}; + + bool get _controllersReady => + animationControllerList.length >= giftMap.length; + + GiftAnimationSlotSnapshot? slotSnapshotAt(int index) { + final gift = giftMap[index]; + if (gift == null || animationControllerList.length <= index) { + return null; + } + final bean = animationControllerList[index]; + return GiftAnimationSlotSnapshot( + index: index, + bean: bean, + sendUserName: gift.sendUserName, + sendToUserName: gift.sendToUserName, + sendUserPic: gift.sendUserPic, + giftPic: gift.giftPic, + giftName: gift.giftName, + giftCount: gift.giftCount, + rewardAmountText: gift.rewardAmountText, + rewardAmount: gift.rewardAmount, + showLuckyRewardFrame: gift.showLuckyRewardFrame, + labelId: gift.labelId, + giftCountStepUnit: gift.giftCountStepUnit, + ); + } + + void enqueueGiftAnimation(LGiftModel giftModel) { + if (_mergeIntoActiveAnimation(giftModel)) { + return; + } + if (_mergeIntoPendingAnimation(giftModel)) { + return; + } + _trimPendingAnimations(); + pendingAnimationsQueue.add(giftModel); + proceedToNextAnimation(); + } + + void _trimPendingAnimations() { + while (pendingAnimationsQueue.length >= _maxPendingAnimations) { + pendingAnimationsQueue.removeFirst(); + } + } + + bool _mergeIntoActiveAnimation(LGiftModel incoming) { + for (final entry in giftMap.entries) { + final current = entry.value; + if (current == null || current.labelId != incoming.labelId) { + continue; + } + _mergeGiftModel(target: current, incoming: incoming); + notifyListeners(); + _refreshSlotAnimation(entry.key, restartEntry: false); + return true; + } + return false; + } + + bool _mergeIntoPendingAnimation(LGiftModel incoming) { + for (final pending in pendingAnimationsQueue) { + if (pending.labelId != incoming.labelId) { + continue; + } + _mergeGiftModel(target: pending, incoming: incoming); + return true; + } + return false; + } + + void _mergeGiftModel({ + required LGiftModel target, + required LGiftModel incoming, + }) { + target.giftCount = (target.giftCount) + (incoming.giftCount); + if (target.sendUserName.isEmpty) { + target.sendUserName = incoming.sendUserName; + } + if (target.sendToUserName.isEmpty) { + target.sendToUserName = incoming.sendToUserName; + } + if (target.sendUserPic.isEmpty) { + target.sendUserPic = incoming.sendUserPic; + } + if (target.giftPic.isEmpty) { + target.giftPic = incoming.giftPic; + } + if (target.giftName.isEmpty) { + target.giftName = incoming.giftName; + } + target.rewardAmount = target.rewardAmount + incoming.rewardAmount; + if (incoming.rewardAmountText.isNotEmpty) { + target.rewardAmountText = incoming.rewardAmountText; + } + target.showLuckyRewardFrame = + target.showLuckyRewardFrame || incoming.showLuckyRewardFrame; + if (incoming.giftCountStepUnit > 0) { + target.giftCountStepUnit = incoming.giftCountStepUnit; + } + } + + ///开始播放 + proceedToNextAnimation() { + if (pendingAnimationsQueue.isEmpty || !_controllersReady) { + return; + } + var playGift = pendingAnimationsQueue.first; + for (var key in giftMap.keys) { + var value = giftMap[key]; + if (value == null) { + giftMap[key] = playGift; + pendingAnimationsQueue.removeFirst(); + notifyListeners(); + _refreshSlotAnimation(key, restartEntry: true); + break; + } else { + if (value.labelId == playGift.labelId) { + _mergeGiftModel(target: value, incoming: playGift); + pendingAnimationsQueue.removeFirst(); + notifyListeners(); + _refreshSlotAnimation(key, restartEntry: false); + break; + } + } + } + } + + void attachAnimationControllers(List anins) { + animationControllerList = anins; + proceedToNextAnimation(); + } + + void cleanupAnimationResources() { + pendingAnimationsQueue.clear(); + giftMap[0] = null; + giftMap[1] = null; + giftMap[2] = null; + giftMap[3] = null; + for (var element in animationControllerList) { + element.dismissTimer?.cancel(); + element.controller.dispose(); + element.countPulseController.dispose(); + } + animationControllerList.clear(); + } + + void markAnimationAsFinished(int index) { + _cancelSlotDismissTimer(index); + if (giftMap[index] == null) { + return; + } + giftMap[index] = null; + notifyListeners(); + proceedToNextAnimation(); + } + + void _refreshSlotAnimation(int index, {required bool restartEntry}) { + if (animationControllerList.length <= index) { + return; + } + final bean = animationControllerList[index]; + _restartSlotDismissTimer(index); + bean.countPulseController.forward(from: 0); + if (restartEntry) { + bean.controller.forward(from: 0); + } + } + + void _restartSlotDismissTimer(int index) { + if (animationControllerList.length <= index) { + return; + } + final bean = animationControllerList[index]; + bean.dismissTimer?.cancel(); + bean.dismissTimer = Timer(_activeComboIdleDismissDelay, () { + if (giftMap[index] == null) { + return; + } + markAnimationAsFinished(index); + }); + } + + void _cancelSlotDismissTimer(int index) { + if (animationControllerList.length <= index) { + return; + } + animationControllerList[index].dismissTimer?.cancel(); + animationControllerList[index].dismissTimer = null; + } +} + +@immutable +class GiftAnimationSlotSnapshot { + const GiftAnimationSlotSnapshot({ + required this.index, + required this.bean, + required this.sendUserName, + required this.sendToUserName, + required this.sendUserPic, + required this.giftPic, + required this.giftName, + required this.giftCount, + required this.rewardAmountText, + required this.rewardAmount, + required this.showLuckyRewardFrame, + required this.labelId, + required this.giftCountStepUnit, + }); + + final int index; + final LGiftScrollingScreenAnimsBean bean; + final String sendUserName; + final String sendToUserName; + final String sendUserPic; + final String giftPic; + final String giftName; + final num giftCount; + final String rewardAmountText; + final num rewardAmount; + final bool showLuckyRewardFrame; + final String labelId; + final num giftCountStepUnit; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is GiftAnimationSlotSnapshot && + other.index == index && + identical(other.bean, bean) && + other.sendUserName == sendUserName && + other.sendToUserName == sendToUserName && + other.sendUserPic == sendUserPic && + other.giftPic == giftPic && + other.giftName == giftName && + other.giftCount == giftCount && + other.rewardAmountText == rewardAmountText && + other.rewardAmount == rewardAmount && + other.showLuckyRewardFrame == showLuckyRewardFrame && + other.labelId == labelId && + other.giftCountStepUnit == giftCountStepUnit; + } + + @override + int get hashCode => Object.hash( + index, + bean, + sendUserName, + sendToUserName, + sendUserPic, + giftPic, + giftName, + giftCount, + rewardAmountText, + rewardAmount, + showLuckyRewardFrame, + labelId, + giftCountStepUnit, + ); +} diff --git a/lib/services/gift/gift_system_manager.dart b/lib/services/gift/gift_system_manager.dart index a02080d..2bc53e9 100644 --- a/lib/services/gift/gift_system_manager.dart +++ b/lib/services/gift/gift_system_manager.dart @@ -27,6 +27,8 @@ class SocialChatGiftSystemManager extends ChangeNotifier { 8000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_8000.svga", 10000: "sc_images/room/anim/luck_gift/luck_gift_combo_count_10000.svga", }; + static final List _luckGiftComboEffectMilestones = + (_luckGiftComboEffectAssets.keys.toList()..sort()); static const List _luckGiftMilestones = [ 10, 20, @@ -211,7 +213,7 @@ class SocialChatGiftSystemManager extends ChangeNotifier { } void startGiftAnimation() { - final milestone = resolveHighestReachedComboMilestone(number); + final milestone = resolveHighestReachedComboEffectMilestone(number); if (milestone == null || (isPlayed[milestone] ?? false)) { return; } @@ -261,19 +263,45 @@ class SocialChatGiftSystemManager extends ChangeNotifier { return resolveHighestReachedComboMilestone(count); } + static int? resolveHighestReachedComboEffectMilestone(num count) { + final normalizedCount = count.floor(); + int? highest; + for (final milestone in _luckGiftComboEffectMilestones) { + if (milestone > normalizedCount) { + break; + } + highest = milestone; + } + return highest; + } + + static int? resolveHighestCrossedComboEffectMilestone({ + required num previousCount, + required num currentCount, + }) { + final previous = previousCount.floor(); + final current = currentCount.floor(); + if (current <= previous) { + return null; + } + int? highest; + for (final milestone in _luckGiftComboEffectMilestones) { + if (milestone <= previous) { + continue; + } + if (milestone > current) { + break; + } + highest = milestone; + } + return highest; + } + static String? resolveComboMilestoneEffectPath(num count) { if (count % 1 != 0) { return null; } - final normalizedCount = count.toInt(); - final comboEffectPath = _luckGiftComboEffectAssets[normalizedCount]; - if (comboEffectPath != null) { - return comboEffectPath; - } - if (normalizedCount > 9999) { - return "sc_images/room/anim/luck_gift_count_5000_mor.mp4"; - } - return "sc_images/room/anim/luck_gift_count_$normalizedCount.mp4"; + return _luckGiftComboEffectAssets[count.toInt()]; } static String? resolveLuckGiftComboEffectPath(num count) { diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index fa628e4..5bd258e 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -568,7 +568,7 @@ class SocialChatUserProfile { ///获取Id,有靓号显示靓号 String getID() { String uid = account ?? ""; - if (_ownSpecialId != null) { + if (hasSpecialId()) { uid = _ownSpecialId?.account ?? ""; } return uid; diff --git a/lib/shared/data_sources/models/message/sc_floating_message.dart b/lib/shared/data_sources/models/message/sc_floating_message.dart index fa89e5d..9c1e63f 100644 --- a/lib/shared/data_sources/models/message/sc_floating_message.dart +++ b/lib/shared/data_sources/models/message/sc_floating_message.dart @@ -7,6 +7,7 @@ class SCFloatingMessage { String? userName; // 用户昵称 String? toUserName; // 用户昵称 String? giftUrl; // 礼物图标 + String? giftId; // 礼物id int? type; int? rocketLevel; num? coins; @@ -25,6 +26,7 @@ class SCFloatingMessage { this.userName = '', this.toUserName = '', this.giftUrl = '', + this.giftId = '', this.number = 0, this.coins = 0, this.priority = 10, @@ -42,6 +44,7 @@ class SCFloatingMessage { userName = json['userName']; toUserName = json['toUserName']; giftUrl = json['giftUrl']; + giftId = json['giftId']; coins = json['coins']; number = json['number']; priority = json['priority']; @@ -60,6 +63,7 @@ class SCFloatingMessage { map['userName'] = userName; map['toUserName'] = toUserName; map['giftUrl'] = giftUrl; + map['giftId'] = giftId; map['coins'] = coins; map['number'] = number; map['priority'] = priority; diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index 5bfd0a0..f4d5bba 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -1,213 +1,287 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/main.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/shared/tools/sc_entrance_vap_svga_manager.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:yumi/ui_kit/widgets/room/floating/floating_game_screen_widget.dart'; -import 'package:yumi/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart'; -import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart'; -import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart'; -import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart'; -import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; - -typedef FloatingScreenManager = OverlayManager; - -class OverlayManager { - final SCPriorityQueue _messageQueue = SCPriorityQueue( - (a, b) => b.priority.compareTo(a.priority), - ); - bool _isPlaying = false; - OverlayEntry? _currentOverlayEntry; - - bool _isProcessing = false; - bool _isDisposed = false; - - static final OverlayManager _instance = OverlayManager._internal(); - - factory OverlayManager() => _instance; - - OverlayManager._internal(); - - void addMessage(SCFloatingMessage message) { - if (_isDisposed) return; - if (SCGlobalConfig.isFloatingAnimationInGlobal) { - _messageQueue.add(message); - _safeScheduleNext(); - } else { - _messageQueue.clear(); - _isPlaying = false; - _isProcessing = false; - _isDisposed = false; - } - } - - void _safeScheduleNext() { - if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; - _isProcessing = true; - - try { - _scheduleNext(); - } finally { - _isProcessing = false; - } - } - - void _scheduleNext() { - if (_isPlaying || _messageQueue.isEmpty || _isDisposed) return; - - try { - final context = navigatorKey.currentState?.context; - if (context == null || !context.mounted) return; - - // 安全地获取第一个消息 - if (_messageQueue.isEmpty) return; - final messageToProcess = _messageQueue.first; - - if (messageToProcess?.type == 1) { - final rtcProvider = Provider.of( - context, - listen: false, - ); - if (rtcProvider.currenRoom == null) { - // 从队列中移除第一个元素 - _messageQueue.removeFirst(); - _safeScheduleNext(); - return; - } - } - - // 安全地移除并播放消息 - final messageToPlay = _messageQueue.removeFirst(); - _playMessage(messageToPlay); - } catch (e) { - debugPrint('播放悬浮消息出错: $e'); - _isPlaying = false; - _safeScheduleNext(); - } - } - - void _playMessage(SCFloatingMessage message) { - _isPlaying = true; - final context = navigatorKey.currentState?.context; - if (context == null || !context.mounted) { - _isPlaying = false; - _safeScheduleNext(); - return; - } - - _currentOverlayEntry = OverlayEntry( - builder: - (_) => Align( - alignment: AlignmentDirectional.topStart, - child: Transform.translate( - offset: Offset(0, 70.w), - child: _buildScreenWidget(message), - ), - ), - ); - - Overlay.of(context).insert(_currentOverlayEntry!); - } - - Widget _buildScreenWidget(SCFloatingMessage message) { - bool completed = false; - - void onComplete() { - if (completed) return; - completed = true; - - try { - _currentOverlayEntry?.remove(); - _currentOverlayEntry = null; - _isPlaying = false; - _safeScheduleNext(); - } catch (e) { - debugPrint('清理悬浮消息出错: $e'); - _isPlaying = false; - _safeScheduleNext(); - } - } - - switch (message.type) { - case 0: - return FloatingLuckGiftScreenWidget( - message: message, - onAnimationCompleted: onComplete, - ); - case 1: - //房间礼物 - return FloatingGiftScreenWidget( - message: message, - onAnimationCompleted: onComplete, - ); - case 2: - //游戏中奖 - return FloatingGameScreenWidget( - message: message, - onAnimationCompleted: onComplete, - ); - case 3: - //火箭 - return FloatingRoomRocketScreenWidget( - message: message, - onAnimationCompleted: onComplete, - ); - case 4: - //红包 - return FloatingRoomRedenvelopeScreenWidget( - message: message, - onAnimationCompleted: onComplete, - ); - default: - onComplete(); - return Container(); - } - } - - void activate() { - _isDisposed = false; - } - - void dispose() { - _isDisposed = true; - _currentOverlayEntry?.remove(); - _currentOverlayEntry = null; - _messageQueue.clear(); - _isPlaying = false; - _isProcessing = false; - } - - void removeRoom() { - // 正确地从 PriorityQueue 中移除特定类型的消息 - _removeMessagesByType(1); - } - - // 辅助方法:移除特定类型的消息 - void _removeMessagesByType(int type) { - // 由于 PriorityQueue 没有直接的方法来移除特定元素, - // 我们需要重建队列,排除指定类型的消息 - final newQueue = SCPriorityQueue( - (a, b) => b.priority.compareTo(a.priority), - ); - - // 遍历原始队列,只添加非指定类型的消息 - while (_messageQueue.isNotEmpty) { - final message = _messageQueue.removeFirst(); - if (message.type != type) { - newQueue.add(message); - } - } - - // 将新队列赋值给原队列 - while (newQueue.isNotEmpty) { - _messageQueue.add(newQueue.removeFirst()); - } - } - - // 辅助方法:获取队列信息 - int get queueLength => _messageQueue.length; - - bool get isPlaying => _isPlaying; -} +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/main.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/shared/tools/sc_network_image_utils.dart'; +import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; +import 'package:yumi/shared/tools/sc_entrance_vap_svga_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_game_screen_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +typedef FloatingScreenManager = OverlayManager; + +class OverlayManager { + final SCPriorityQueue _messageQueue = SCPriorityQueue( + (a, b) => b.priority.compareTo(a.priority), + ); + bool _isPlaying = false; + OverlayEntry? _currentOverlayEntry; + SCFloatingMessage? _currentMessage; + + bool _isProcessing = false; + bool _isDisposed = false; + + static final OverlayManager _instance = OverlayManager._internal(); + + factory OverlayManager() => _instance; + + OverlayManager._internal(); + + void addMessage(SCFloatingMessage message) { + if (_isDisposed) return; + if (SCGlobalConfig.isFloatingAnimationInGlobal) { + unawaited(_warmMessageImages(message)); + SCRoomEffectScheduler().scheduleDeferredEffect( + debugLabel: 'floating_message_type_${message.type ?? -1}', + action: () { + if (_isDisposed) { + return; + } + _messageQueue.add(message); + _safeScheduleNext(); + }, + ); + } else { + _messageQueue.clear(); + _isPlaying = false; + _isProcessing = false; + _isDisposed = false; + } + } + + Future _warmMessageImages(SCFloatingMessage message) async { + unawaited( + warmImageResource( + message.userAvatarUrl ?? "", + logicalWidth: 80, + logicalHeight: 80, + ), + ); + unawaited( + warmImageResource( + message.toUserAvatarUrl ?? "", + logicalWidth: 80, + logicalHeight: 80, + ), + ); + unawaited( + warmImageResource( + message.giftUrl ?? "", + logicalWidth: 96, + logicalHeight: 96, + ), + ); + } + + void _safeScheduleNext() { + if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; + _isProcessing = true; + + try { + _scheduleNext(); + } finally { + _isProcessing = false; + } + } + + void _scheduleNext() { + if (_isPlaying || _messageQueue.isEmpty || _isDisposed) return; + + try { + final context = navigatorKey.currentState?.context; + if (context == null || !context.mounted) return; + + // 安全地获取第一个消息 + if (_messageQueue.isEmpty) return; + final messageToProcess = _messageQueue.first; + + if (!_shouldDisplayMessage(context, messageToProcess)) { + _messageQueue.removeFirst(); + _safeScheduleNext(); + return; + } + + // 安全地移除并播放消息 + final messageToPlay = _messageQueue.removeFirst(); + _playMessage(messageToPlay); + } catch (e) { + debugPrint('播放悬浮消息出错: $e'); + _isPlaying = false; + _safeScheduleNext(); + } + } + + void _playMessage(SCFloatingMessage message) { + _isPlaying = true; + _currentMessage = message; + final context = navigatorKey.currentState?.context; + if (context == null || !context.mounted) { + _isPlaying = false; + _currentMessage = null; + _safeScheduleNext(); + return; + } + + _currentOverlayEntry = OverlayEntry( + builder: + (_) => Align( + alignment: AlignmentDirectional.topStart, + child: Transform.translate( + offset: Offset(0, 70.w), + child: RepaintBoundary(child: _buildScreenWidget(message)), + ), + ), + ); + + Overlay.of(context).insert(_currentOverlayEntry!); + } + + Widget _buildScreenWidget(SCFloatingMessage message) { + bool completed = false; + + void onComplete() { + if (completed) return; + completed = true; + + try { + _currentOverlayEntry?.remove(); + _currentOverlayEntry = null; + _isPlaying = false; + _currentMessage = null; + _safeScheduleNext(); + } catch (e) { + debugPrint('清理悬浮消息出错: $e'); + _isPlaying = false; + _currentMessage = null; + _safeScheduleNext(); + } + } + + switch (message.type) { + case 0: + return FloatingLuckGiftScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + case 1: + //房间礼物 + return FloatingGiftScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + case 2: + //游戏中奖 + return FloatingGameScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + case 3: + //火箭 + return FloatingRoomRocketScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + case 4: + //红包 + return FloatingRoomRedenvelopeScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + default: + onComplete(); + return Container(); + } + } + + void activate() { + _isDisposed = false; + } + + void dispose() { + _isDisposed = true; + _currentOverlayEntry?.remove(); + _currentOverlayEntry = null; + _currentMessage = null; + _messageQueue.clear(); + _isPlaying = false; + _isProcessing = false; + } + + void removeRoom() { + _removeActiveRoomMessage(); + _removeMessagesByType(1); + _removeMessagesByType(0); + } + + // 辅助方法:移除特定类型的消息 + void _removeMessagesByType(int type) { + // 由于 PriorityQueue 没有直接的方法来移除特定元素, + // 我们需要重建队列,排除指定类型的消息 + final newQueue = SCPriorityQueue( + (a, b) => b.priority.compareTo(a.priority), + ); + + // 遍历原始队列,只添加非指定类型的消息 + while (_messageQueue.isNotEmpty) { + final message = _messageQueue.removeFirst(); + if (message.type != type) { + newQueue.add(message); + } + } + + // 将新队列赋值给原队列 + while (newQueue.isNotEmpty) { + _messageQueue.add(newQueue.removeFirst()); + } + } + + // 辅助方法:获取队列信息 + int get queueLength => _messageQueue.length; + + bool get isPlaying => _isPlaying; + + bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) { + if (message == null) { + return false; + } + if (message.type != 0 && message.type != 1) { + return true; + } + + final rtcProvider = Provider.of( + context, + listen: false, + ); + if (!rtcProvider.shouldShowRoomVisualEffects) { + return false; + } + final currentRoomId = + rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; + final messageRoomId = (message.roomId ?? "").trim(); + if (currentRoomId.isEmpty || messageRoomId.isEmpty) { + return false; + } + return currentRoomId == messageRoomId; + } + + void _removeActiveRoomMessage() { + final activeMessage = _currentMessage; + if (activeMessage == null || + (activeMessage.type != 0 && activeMessage.type != 1)) { + return; + } + _currentOverlayEntry?.remove(); + _currentOverlayEntry = null; + _currentMessage = null; + _isPlaying = false; + _safeScheduleNext(); + } +} diff --git a/lib/shared/data_sources/sources/remote/net/api.dart b/lib/shared/data_sources/sources/remote/net/api.dart index cee2079..2756327 100644 --- a/lib/shared/data_sources/sources/remote/net/api.dart +++ b/lib/shared/data_sources/sources/remote/net/api.dart @@ -130,6 +130,7 @@ class BaseNetworkClient { String path, { Map? queryParams, Map? extra, + bool allowNullBody = false, required T Function(dynamic) fromJson, CancelToken? cancelToken, ProgressCallback? onSendProgress, @@ -140,6 +141,7 @@ class BaseNetworkClient { method: 'GET', queryParams: queryParams, extra: extra, + allowNullBody: allowNullBody, fromJson: fromJson, cancelToken: cancelToken, onSendProgress: onSendProgress, @@ -153,6 +155,7 @@ class BaseNetworkClient { dynamic data, Map? queryParams, Map? extra, + bool allowNullBody = false, required T Function(dynamic) fromJson, CancelToken? cancelToken, ProgressCallback? onSendProgress, @@ -164,6 +167,7 @@ class BaseNetworkClient { data: data, queryParams: queryParams, extra: extra, + allowNullBody: allowNullBody, fromJson: fromJson, cancelToken: cancelToken, onSendProgress: onSendProgress, @@ -177,6 +181,7 @@ class BaseNetworkClient { dynamic data, Map? queryParams, Map? extra, + bool allowNullBody = false, required T Function(dynamic) fromJson, CancelToken? cancelToken, ProgressCallback? onSendProgress, @@ -188,6 +193,7 @@ class BaseNetworkClient { data: data, queryParams: queryParams, extra: extra, + allowNullBody: allowNullBody, fromJson: fromJson, cancelToken: cancelToken, onSendProgress: onSendProgress, @@ -201,6 +207,7 @@ class BaseNetworkClient { dynamic data, Map? queryParams, Map? extra, + bool allowNullBody = false, required T Function(dynamic) fromJson, CancelToken? cancelToken, ProgressCallback? onSendProgress, @@ -212,6 +219,7 @@ class BaseNetworkClient { data: data, queryParams: queryParams, extra: extra, + allowNullBody: allowNullBody, fromJson: fromJson, cancelToken: cancelToken, onSendProgress: onSendProgress, @@ -226,6 +234,7 @@ class BaseNetworkClient { dynamic data, Map? queryParams, Map? extra, + bool allowNullBody = false, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, @@ -251,6 +260,8 @@ class BaseNetworkClient { if (baseResponse.success) { if (baseResponse.body != null) { return baseResponse.body!; + } else if (allowNullBody && null is T) { + return null as T; } else { if (T == bool) return false as T; if (T == int) return 0 as T; diff --git a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart index 7e48c46..f64bc4f 100644 --- a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart @@ -213,6 +213,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository { final result = await http.post( "11335ecd55d99c9100f5cd070e5b73f1574aff257ce7e668d08f4caccd1c6232", data: {"roomId": roomId, "mickIndex": mickIndex}, + allowNullBody: true, fromJson: (json) => null, ); return result; @@ -224,6 +225,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository { final result = await http.post( "70ffb9681e0103463b1faf883935e55d", data: {"roomId": roomId, "mickIndex": mickIndex, "lock": lock}, + allowNullBody: true, fromJson: (json) => null, ); return result; @@ -405,6 +407,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository { final result = await http.post( "68900c787a89c1092b21ee1a610ef323380d4f62784dacdb4e3d4e1f60770a39", data: {"roomId": roomId, "userId": userId}, + allowNullBody: true, fromJson: (json) => null, ); return result; 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 5b26365..ad6b346 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 @@ -348,6 +348,7 @@ class SCAccountRepository implements SocialChatUserRepository { final result = await http.post( "745ce65f1d68f106702ad3c636aca0e0ac57f82127b3ac414551924946593db7", data: {"pwd": pwd}, + allowNullBody: true, fromJson: (json) => null, ); return result; diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 340487f..eda1c7d 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_svga/flutter_svga.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; import 'package:tancent_vap/utils/constant.dart'; import 'package:tancent_vap/widgets/vap_view.dart'; @@ -267,6 +268,9 @@ class SCGiftVapSvgaManager { if (_dis) { return; } + SCRoomEffectScheduler().completeHighCostTask( + debugLabel: _currentTask?.path, + ); _play = false; _currentTask = null; _scheduleNextTask(delay: delay); @@ -316,6 +320,9 @@ class SCGiftVapSvgaManager { if (status.isCompleted) { _log('svga completed path=${_currentTask?.path}'); _rsc?.reset(); + SCRoomEffectScheduler().completeHighCostTask( + debugLabel: _currentTask?.path, + ); _play = false; _currentTask = null; _pn(); @@ -363,8 +370,13 @@ class SCGiftVapSvgaManager { return; } _tq.add(task); + SCRoomEffectScheduler().registerHighCostTaskQueued(debugLabel: path); + unawaited(preload(path)); while (_tq.length > _maxPendingTaskCount) { final removedTask = _tq.removeLast(); + SCRoomEffectScheduler().completeHighCostTask( + debugLabel: removedTask.path, + ); _log( 'trim queued task path=${removedTask.path} ' 'priority=${removedTask.priority} queue=${_tq.length}', @@ -547,6 +559,7 @@ class SCGiftVapSvgaManager { _rsc?.stop(); _rsc?.reset(); _rsc?.videoItem = null; + SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback'); } // 释放资源 diff --git a/lib/shared/tools/sc_network_image_utils.dart b/lib/shared/tools/sc_network_image_utils.dart index 80606c2..b841d7a 100644 --- a/lib/shared/tools/sc_network_image_utils.dart +++ b/lib/shared/tools/sc_network_image_utils.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:ui'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/painting.dart'; @@ -37,26 +38,56 @@ Map? buildNetworkImageHeaders(String url) { return headers; } -ImageProvider buildCachedImageProvider(String url) { +int? _resolveImageProviderCacheDimension(double? logicalSize) { + if (logicalSize == null || !logicalSize.isFinite || logicalSize <= 0) { + return null; + } + final devicePixelRatio = + PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 2.0; + final maxDimension = SCGlobalConfig.isLowPerformanceDevice ? 1280 : 1920; + final pixelSize = (logicalSize * devicePixelRatio).round(); + return pixelSize.clamp(1, maxDimension).toInt(); +} + +ImageProvider buildCachedImageProvider( + String url, { + double? logicalWidth, + double? logicalHeight, +}) { + late final ImageProvider provider; if (url.startsWith("http")) { - return ExtendedNetworkImageProvider( + provider = ExtendedNetworkImageProvider( url, headers: buildNetworkImageHeaders(url), cache: true, cacheMaxAge: const Duration(days: 7), ); + } else if (url.startsWith('/')) { + provider = FileImage(File(url)); + } else { + provider = AssetImage(url); } - return FileImage(File(url)); + return ResizeImage.resizeIfNeeded( + _resolveImageProviderCacheDimension(logicalWidth), + _resolveImageProviderCacheDimension(logicalHeight), + provider, + ); } -Future warmNetworkImage( - String url, { +Future warmImageResource( + String resource, { + double? logicalWidth, + double? logicalHeight, Duration timeout = const Duration(seconds: 6), }) async { - if (url.isEmpty) { + if (resource.isEmpty) { return false; } - final provider = buildCachedImageProvider(url); + final provider = buildCachedImageProvider( + resource, + logicalWidth: logicalWidth, + logicalHeight: logicalHeight, + ); final stream = provider.resolve(ImageConfiguration.empty); final completer = Completer(); late final ImageStreamListener listener; @@ -83,3 +114,17 @@ Future warmNetworkImage( }, ); } + +Future warmNetworkImage( + String url, { + double? logicalWidth, + double? logicalHeight, + Duration timeout = const Duration(seconds: 6), +}) { + return warmImageResource( + url, + logicalWidth: logicalWidth, + logicalHeight: logicalHeight, + timeout: timeout, + ); +} diff --git a/lib/shared/tools/sc_room_effect_scheduler.dart b/lib/shared/tools/sc_room_effect_scheduler.dart new file mode 100644 index 0000000..a25fc6e --- /dev/null +++ b/lib/shared/tools/sc_room_effect_scheduler.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; + +class SCRoomEffectScheduler { + static const Duration _deferredDrainInterval = Duration(milliseconds: 60); + + static final SCRoomEffectScheduler _instance = + SCRoomEffectScheduler._internal(); + + factory SCRoomEffectScheduler() => _instance; + + SCRoomEffectScheduler._internal(); + + final Queue<_DeferredRoomEffectTask> _deferredTasks = + Queue<_DeferredRoomEffectTask>(); + + Timer? _deferredDrainTimer; + int _pendingHighCostTaskCount = 0; + int _nextTaskId = 0; + + bool get hasActiveHighCostEffects => _pendingHighCostTaskCount > 0; + + void registerHighCostTaskQueued({String? debugLabel}) { + _pendingHighCostTaskCount += 1; + _cancelDeferredDrain(); + _log( + 'register_high_cost task=${debugLabel ?? "unknown"} ' + 'pending=$_pendingHighCostTaskCount deferred=${_deferredTasks.length}', + ); + } + + void completeHighCostTask({String? debugLabel}) { + if (_pendingHighCostTaskCount > 0) { + _pendingHighCostTaskCount -= 1; + } + _log( + 'complete_high_cost task=${debugLabel ?? "unknown"} ' + 'pending=$_pendingHighCostTaskCount deferred=${_deferredTasks.length}', + ); + _scheduleDeferredDrainIfNeeded(); + } + + void clearHighCostTasks({String reason = 'unknown'}) { + if (_pendingHighCostTaskCount == 0) { + _scheduleDeferredDrainIfNeeded(); + return; + } + _pendingHighCostTaskCount = 0; + _log( + 'clear_high_cost reason=$reason ' + 'pending=$_pendingHighCostTaskCount deferred=${_deferredTasks.length}', + ); + _scheduleDeferredDrainIfNeeded(); + } + + void scheduleDeferredEffect({ + required String debugLabel, + required VoidCallback action, + }) { + if (!hasActiveHighCostEffects) { + action(); + return; + } + + final task = _DeferredRoomEffectTask( + id: _nextTaskId++, + debugLabel: debugLabel, + action: action, + ); + _deferredTasks.add(task); + _log( + 'defer task=${task.debugLabel} id=${task.id} ' + 'pending=$_pendingHighCostTaskCount deferred=${_deferredTasks.length}', + ); + } + + void clearDeferredTasks({String reason = 'unknown'}) { + if (_deferredTasks.isEmpty) { + _cancelDeferredDrain(); + return; + } + _deferredTasks.clear(); + _cancelDeferredDrain(); + _log('clear_deferred reason=$reason'); + } + + void _scheduleDeferredDrainIfNeeded() { + if (hasActiveHighCostEffects || + _deferredTasks.isEmpty || + _deferredDrainTimer != null) { + return; + } + _deferredDrainTimer = Timer(_deferredDrainInterval, _drainNextDeferredTask); + } + + void _drainNextDeferredTask() { + _deferredDrainTimer = null; + if (hasActiveHighCostEffects || _deferredTasks.isEmpty) { + return; + } + + final task = _deferredTasks.removeFirst(); + _log( + 'drain_deferred task=${task.debugLabel} id=${task.id} ' + 'remaining=${_deferredTasks.length}', + ); + try { + task.action(); + } catch (error, stackTrace) { + debugPrint( + '[RoomEffectScheduler] deferred task failed: $error\n$stackTrace', + ); + } + + _scheduleDeferredDrainIfNeeded(); + } + + void _cancelDeferredDrain() { + _deferredDrainTimer?.cancel(); + _deferredDrainTimer = null; + } + + void _log(String message) { + debugPrint('[RoomEffectScheduler] $message'); + } +} + +class _DeferredRoomEffectTask { + const _DeferredRoomEffectTask({ + required this.id, + required this.debugLabel, + required this.action, + }); + + final int id; + final String debugLabel; + final VoidCallback action; +} diff --git a/lib/ui_kit/widgets/id/sc_special_id_badge.dart b/lib/ui_kit/widgets/id/sc_special_id_badge.dart new file mode 100644 index 0000000..21c0e01 --- /dev/null +++ b/lib/ui_kit/widgets/id/sc_special_id_badge.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; + +class SCSpecialIdAssets { + static const String roomId = 'sc_images/general/room_id.svga'; + static const String roomOwnerId = 'sc_images/general/room_id_custom.svga'; + static const String userId = 'sc_images/general/user_id.svga'; + static const String userIdLarge = 'sc_images/general/user_id_4.svga'; +} + +class SCSpecialIdBadge extends StatelessWidget { + const SCSpecialIdBadge({ + super.key, + required this.idText, + this.showAnimated = false, + this.assetPath, + this.animationWidth = 120, + this.animationHeight = 28, + this.textPadding = EdgeInsets.zero, + this.animationTextStyle, + this.normalTextStyle, + this.normalPrefix = 'ID:', + this.showCopyIcon = false, + this.copyIconAssetPath = 'sc_images/room/sc_icon_user_card_copy_id.png', + this.copyIconSize = 12, + this.copyIconSpacing = 5, + this.copyIconColor, + this.loop = true, + this.active = true, + this.animationFit = BoxFit.fill, + }); + + final String idText; + final bool showAnimated; + final String? assetPath; + final double animationWidth; + final double animationHeight; + final EdgeInsetsGeometry textPadding; + final TextStyle? animationTextStyle; + final TextStyle? normalTextStyle; + final String normalPrefix; + final bool showCopyIcon; + final String copyIconAssetPath; + final double copyIconSize; + final double copyIconSpacing; + final Color? copyIconColor; + final bool loop; + final bool active; + final BoxFit animationFit; + + @override + Widget build(BuildContext context) { + final normalizedId = idText.trim(); + final shouldAnimate = + showAnimated && + (assetPath?.isNotEmpty ?? false) && + normalizedId.isNotEmpty; + + final children = [ + shouldAnimate + ? _buildAnimatedBadge(normalizedId) + : _buildPlainBadge(normalizedId), + ]; + + if (showCopyIcon) { + children.add(SizedBox(width: copyIconSpacing)); + children.add( + Image.asset( + copyIconAssetPath, + width: copyIconSize, + height: copyIconSize, + color: copyIconColor, + ), + ); + } + + return Row( + mainAxisSize: MainAxisSize.min, + textDirection: TextDirection.ltr, + children: children, + ); + } + + Widget _buildAnimatedBadge(String normalizedId) { + return SizedBox( + width: animationWidth, + height: animationHeight, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: SCSvgaAssetWidget( + assetPath: assetPath!, + width: animationWidth, + height: animationHeight, + active: active, + loop: loop, + fit: animationFit, + fallback: const SizedBox.shrink(), + ), + ), + Positioned.fill( + child: Padding( + padding: textPadding, + child: Align( + alignment: Alignment.centerLeft, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + normalizedId, + maxLines: 1, + overflow: TextOverflow.visible, + style: + animationTextStyle ?? + const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildPlainBadge(String normalizedId) { + return Text( + '$normalPrefix$normalizedId', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: + normalTextStyle ?? + const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart b/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart index 87609c6..51a0dcd 100644 --- a/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart +++ b/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app_localizations.dart'; @@ -19,62 +21,145 @@ class LGiftAnimalPage extends StatefulWidget { class _GiftAnimalPageState extends State with TickerProviderStateMixin { - static const String _luckyGiftRewardFrameAssetPath = - "sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga"; late final GiftAnimationManager _giftAnimationManager; @override Widget build(BuildContext context) { return SizedBox( height: 332.w, - child: Consumer( - builder: (context, ref, child) { - return Stack( - clipBehavior: Clip.none, - children: List.generate( - 4, - (index) => _buildGiftTickerItem(context, ref, index), - ), - ); - }, + child: Stack( + clipBehavior: Clip.none, + children: List.generate(4, (index) => _GiftTickerSlot(index: index)), ), ); } - Widget _buildGiftTickerItem( - BuildContext context, - GiftAnimationManager ref, - int index, - ) { - final gift = ref.giftMap[index]; - if (gift == null || ref.animationControllerList.length <= index) { - return const SizedBox.shrink(); + @override + void dispose() { + _giftAnimationManager.cleanupAnimationResources(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _giftAnimationManager = Provider.of( + context, + listen: false, + ); + initAnimal(); + } + + void initAnimal() { + List beans = []; + double top = 60; + for (int i = 0; i < 4; i++) { + var bean = LGiftScrollingScreenAnimsBean(); + var controller = AnimationController( + value: 0, + duration: const Duration(milliseconds: 360), + vsync: this, + ); + bean.controller = controller; + bean.countPulseController = AnimationController( + value: 1, + duration: const Duration(milliseconds: 280), + vsync: this, + ); + bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top); + bean.verticalAnimation = EdgeInsetsTween( + begin: EdgeInsets.only(top: top), + end: EdgeInsets.only(top: 0), + ).animate( + CurvedAnimation(parent: controller, curve: Curves.easeOutCubic), + ); + bean.sizeAnimation = TweenSequence([ + TweenSequenceItem( + tween: Tween( + begin: 1, + end: 1.18, + ).chain(CurveTween(curve: Curves.easeOutCubic)), + weight: 65, + ), + TweenSequenceItem( + tween: Tween( + begin: 1.18, + end: 1, + ).chain(CurveTween(curve: Curves.easeInCubic)), + weight: 35, + ), + ]).animate( + CurvedAnimation( + parent: bean.countPulseController, + curve: Curves.linear, + ), + ); + beans.add(bean); + top = top + 70; } - final bean = ref.animationControllerList[index]; - return AnimatedBuilder( - animation: bean.controller, - builder: (context, child) { - final showLuckyRewardFrame = gift.showLuckyRewardFrame; - final tickerMargin = - showLuckyRewardFrame - ? bean.luckyGiftPinnedMargin - : bean.verticalAnimation.value; - return Container( - margin: tickerMargin, - width: - ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.96 : 0.78), - child: _buildGiftTickerCard(context, gift, bean.sizeAnimation.value), + _giftAnimationManager.attachAnimationControllers(beans); + } + + String getBg(String type) { + return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png"; + } +} + +class _GiftTickerSlot extends StatelessWidget { + const _GiftTickerSlot({required this.index}); + + final int index; + + @override + Widget build(BuildContext context) { + return Selector( + selector: (context, manager) => manager.slotSnapshotAt(index), + builder: (context, snapshot, child) { + if (snapshot == null) { + return const SizedBox.shrink(); + } + return RepaintBoundary( + child: AnimatedBuilder( + animation: Listenable.merge([ + snapshot.bean.controller, + snapshot.bean.countPulseController, + ]), + builder: (context, child) { + final showLuckyRewardFrame = snapshot.showLuckyRewardFrame; + final tickerMargin = + showLuckyRewardFrame + ? snapshot.bean.luckyGiftPinnedMargin + : snapshot.bean.verticalAnimation.value; + return Container( + margin: tickerMargin, + width: + ScreenUtil().screenWidth * + (showLuckyRewardFrame ? 0.96 : 0.78), + child: _GiftTickerCard( + snapshot: snapshot, + animatedSize: 22.sp * snapshot.bean.sizeAnimation.value, + ), + ); + }, + ), ); }, ); } +} - Widget _buildGiftTickerCard( - BuildContext context, - LGiftModel gift, - double animatedSize, - ) { - final showLuckyRewardFrame = gift.showLuckyRewardFrame; +class _GiftTickerCard extends StatelessWidget { + const _GiftTickerCard({required this.snapshot, required this.animatedSize}); + + static const String _luckyGiftRewardFrameAssetPath = + "sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga"; + + final GiftAnimationSlotSnapshot snapshot; + final double animatedSize; + + @override + Widget build(BuildContext context) { + final showLuckyRewardFrame = snapshot.showLuckyRewardFrame; return SizedBox( height: 52.w, child: Stack( @@ -91,16 +176,18 @@ class _GiftAnimalPageState extends State top: 3.w, bottom: 3.w, ), - decoration: BoxDecoration( + decoration: const BoxDecoration( image: DecorationImage( - image: AssetImage(getBg("")), + image: AssetImage( + "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png", + ), fit: BoxFit.fill, ), ), child: Row( children: [ netImage( - url: gift.sendUserPic, + url: snapshot.sendUserPic, shape: BoxShape.circle, width: 26.w, height: 26.w, @@ -114,11 +201,11 @@ class _GiftAnimalPageState extends State children: [ socialchatNickNameText( maxWidth: 88.w, - gift.sendUserName, + snapshot.sendUserName, fontSize: 12.sp, fontWeight: FontWeight.w500, type: "", - needScroll: gift.sendUserName.characters.length > 8, + needScroll: snapshot.sendUserName.characters.length > 8, ), SizedBox(height: 1.w), Row( @@ -133,7 +220,7 @@ class _GiftAnimalPageState extends State ), Flexible( child: Text( - gift.sendToUserName, + snapshot.sendToUserName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -165,25 +252,25 @@ class _GiftAnimalPageState extends State crossAxisAlignment: CrossAxisAlignment.center, children: [ netImage( - url: gift.giftPic, + url: snapshot.giftPic, fit: BoxFit.cover, borderRadius: BorderRadius.circular(4.w), width: 34.w, height: 34.w, ), - if (gift.giftCount > 0) ...[ + if (snapshot.giftCount > 0) ...[ SizedBox(width: 8.w), Flexible( child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, - child: _buildGiftCountLabel(gift, animatedSize), + child: _buildGiftCountLabel(snapshot.giftCount), ), ), ], if (showLuckyRewardFrame) ...[ SizedBox(width: 6.w), - _buildLuckyGiftRewardFrame(gift), + _buildLuckyGiftRewardFrame(), ], ], ), @@ -194,9 +281,7 @@ class _GiftAnimalPageState extends State ); } - Widget _buildGiftCountLabel(LGiftModel gift, double animatedSize) { - final xFontSize = animatedSize; - final countFontSize = animatedSize; + Widget _buildGiftCountLabel(num giftCount) { return Directionality( textDirection: TextDirection.ltr, child: Row( @@ -206,7 +291,7 @@ class _GiftAnimalPageState extends State Text( "x", style: TextStyle( - fontSize: xFontSize, + fontSize: animatedSize, fontStyle: FontStyle.italic, color: const Color(0xFFFFD400), fontWeight: FontWeight.bold, @@ -214,10 +299,11 @@ class _GiftAnimalPageState extends State ), ), SizedBox(width: 2.w), - Text( - _giftCountText(gift.giftCount), + _AnimatedGiftCountText( + targetCount: giftCount, + stepUnit: snapshot.giftCountStepUnit, style: TextStyle( - fontSize: countFontSize, + fontSize: animatedSize, fontStyle: FontStyle.italic, color: const Color(0xFFFFD400), fontWeight: FontWeight.bold, @@ -229,21 +315,23 @@ class _GiftAnimalPageState extends State ); } - Widget _buildLuckyGiftRewardFrame(LGiftModel gift) { + Widget _buildLuckyGiftRewardFrame() { return SizedBox( width: 108.w, height: 52.w, child: Stack( alignment: Alignment.center, children: [ - SCSvgaAssetWidget( - assetPath: _luckyGiftRewardFrameAssetPath, - width: 108.w, - height: 52.w, - fit: BoxFit.cover, - loop: true, - allowDrawingOverflow: true, - fallback: _buildLuckyGiftRewardFrameFallback(), + RepaintBoundary( + child: SCSvgaAssetWidget( + assetPath: _luckyGiftRewardFrameAssetPath, + width: 108.w, + height: 52.w, + fit: BoxFit.cover, + loop: true, + allowDrawingOverflow: true, + fallback: const _LuckyGiftRewardFrameFallback(), + ), ), Padding( padding: EdgeInsetsDirectional.only( @@ -254,7 +342,7 @@ class _GiftAnimalPageState extends State child: FittedBox( fit: BoxFit.scaleDown, child: Text( - "+${_giftRewardAmountText(gift)}", + "+${_giftRewardAmountText()}", maxLines: 1, style: TextStyle( fontSize: 16.sp, @@ -278,7 +366,26 @@ class _GiftAnimalPageState extends State ); } - Widget _buildLuckyGiftRewardFrameFallback() { + String _giftRewardAmountText() { + final rewardAmount = snapshot.rewardAmount; + if (rewardAmount > 0) { + if (rewardAmount > 9999) { + return "${(rewardAmount / 1000).toStringAsFixed(0)}k"; + } + if (rewardAmount % 1 == 0) { + return rewardAmount.toInt().toString(); + } + return rewardAmount.toString(); + } + return snapshot.rewardAmountText; + } +} + +class _LuckyGiftRewardFrameFallback extends StatelessWidget { + const _LuckyGiftRewardFrameFallback(); + + @override + Widget build(BuildContext context) { return Container( width: 108.w, height: 52.w, @@ -300,118 +407,154 @@ class _GiftAnimalPageState extends State ), ); } +} - String _giftCountText(num count) { - return count % 1 == 0 ? count.toInt().toString() : count.toString(); - } +class _AnimatedGiftCountText extends StatefulWidget { + const _AnimatedGiftCountText({ + required this.targetCount, + required this.stepUnit, + required this.style, + }); - String _giftRewardAmountText(LGiftModel gift) { - final rewardAmount = gift.rewardAmount; - if (rewardAmount > 0) { - if (rewardAmount > 9999) { - return "${(rewardAmount / 1000).toStringAsFixed(0)}k"; - } - if (rewardAmount % 1 == 0) { - return rewardAmount.toInt().toString(); - } - return rewardAmount.toString(); - } - return gift.rewardAmountText; - } + final num targetCount; + final num stepUnit; + final TextStyle style; @override - void dispose() { - _giftAnimationManager.cleanupAnimationResources(); - super.dispose(); - } + State<_AnimatedGiftCountText> createState() => _AnimatedGiftCountTextState(); +} + +class _AnimatedGiftCountTextState extends State<_AnimatedGiftCountText> { + Timer? _stepTimer; + late double _displayValue; @override void initState() { super.initState(); - _giftAnimationManager = Provider.of( - context, - listen: false, - ); - initAnimal(); + _displayValue = _resolveInitialDisplayValue(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + _startSteppedAnimation(widget.targetCount.toDouble()); + }); } - void initAnimal() { - List beans = []; - double top = 60; - for (int i = 0; i < 4; i++) { - var bean = LGiftScrollingScreenAnimsBean(); - var controller = AnimationController( - value: 0, - duration: const Duration(milliseconds: 5000), - vsync: this, - ); - bean.controller = controller; - bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top); - // bean.transverseAnimation = Tween( - // begin: Offset(ScreenUtil().screenWidth, 0), - // end: Offset(0, 0), - // ).animate( - // CurvedAnimation( - // parent: controller, - // curve: Interval(0.0, 0.45, curve: Curves.ease), - // ), - // ); - bean.verticalAnimation = EdgeInsetsTween( - begin: EdgeInsets.only(top: top), - end: EdgeInsets.only(top: 0), - ).animate( - CurvedAnimation( - parent: controller, - curve: Interval(0.2, 1, curve: Curves.easeOut), - ), - ); - bean.sizeAnimation = Tween(begin: 0, end: 22).animate( - CurvedAnimation( - parent: controller, - curve: Interval(0.45, 0.55, curve: Curves.ease), - ), - ); - beans.add(bean); - top = top + 70; + @override + void didUpdateWidget(covariant _AnimatedGiftCountText oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.targetCount == widget.targetCount && + oldWidget.stepUnit == widget.stepUnit) { + return; } - beans[0].controller.addStatusListener((state) { - if (state == AnimationStatus.completed) { - //动画完成监听 - _giftAnimationManager.markAnimationAsFinished(0); - } - }); - beans[1].controller.addStatusListener((state) { - if (state == AnimationStatus.completed) { - //动画完成监听 - _giftAnimationManager.markAnimationAsFinished(1); - } - }); - beans[2].controller.addStatusListener((state) { - if (state == AnimationStatus.completed) { - //动画完成监听 - _giftAnimationManager.markAnimationAsFinished(2); - } - }); - beans[3].controller.addStatusListener((state) { - if (state == AnimationStatus.completed) { - //动画完成监听 - _giftAnimationManager.markAnimationAsFinished(3); - } - }); - _giftAnimationManager.attachAnimationControllers(beans); + _startSteppedAnimation(widget.targetCount.toDouble()); } - String getBg(String type) { - return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png"; + @override + void dispose() { + _stepTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text(_formatGiftCount(_resolvedDisplayCount()), style: widget.style); + } + + double _resolveInitialDisplayValue() { + final targetValue = widget.targetCount.toDouble(); + final stepUnit = _resolveStepUnit(targetValue.abs()); + if (targetValue <= 0 || stepUnit <= 0) { + return targetValue; + } + return targetValue <= stepUnit ? targetValue : stepUnit; + } + + void _startSteppedAnimation(double nextValue) { + _stepTimer?.cancel(); + final delta = nextValue - _displayValue; + if (delta.abs() < 0.0001) { + return; + } + if (delta < 0) { + if (!mounted) { + return; + } + setState(() { + _displayValue = nextValue; + }); + return; + } + + final stepUnit = _resolveStepUnit(delta.abs()); + if (stepUnit <= 0) { + if (!mounted) { + return; + } + setState(() { + _displayValue = nextValue; + }); + return; + } + + var tickCount = (delta.abs() / stepUnit).ceil(); + if (tickCount <= 1) { + if (!mounted) { + return; + } + setState(() { + _displayValue = nextValue; + }); + return; + } + tickCount = tickCount.clamp(2, 40); + final increment = delta / tickCount; + final intervalMs = ((700 / tickCount).round()).clamp(28, 80); + var completedTicks = 0; + _stepTimer = Timer.periodic(Duration(milliseconds: intervalMs), (timer) { + if (!mounted) { + timer.cancel(); + return; + } + completedTicks += 1; + final reachedEnd = completedTicks >= tickCount; + setState(() { + _displayValue = reachedEnd ? nextValue : (_displayValue + increment); + }); + if (reachedEnd) { + timer.cancel(); + _stepTimer = null; + } + }); + } + + double _resolveStepUnit(double delta) { + final candidate = widget.stepUnit.abs().toDouble(); + if (candidate <= 0) { + return delta; + } + return candidate.clamp(0.0001, delta); + } + + num _resolvedDisplayCount() { + if (widget.targetCount % 1 == 0) { + return _displayValue.round(); + } + return double.parse(_displayValue.toStringAsFixed(1)); } } +String _formatGiftCount(num count) { + return count % 1 == 0 ? count.toInt().toString() : count.toString(); +} + class LGiftScrollingScreenAnimsBean { - // late Animation transverseAnimation; late Animation verticalAnimation; late EdgeInsets luckyGiftPinnedMargin; late AnimationController controller; + late AnimationController countPulseController; late Animation sizeAnimation; + Timer? dismissTimer; } class LGiftModel { @@ -444,4 +587,7 @@ class LGiftModel { //id String labelId = ""; + + // 数量视觉步长。单次点击送 1 个时会按 1 递增;单次点击送 25 个时直接按 25 跳。 + num giftCountStepUnit = 1; } diff --git a/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart b/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart index 8650bd2..50317da 100644 --- a/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart +++ b/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart @@ -1,325 +1,351 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:yumi/services/audio/rtm_manager.dart'; -import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; - -class RoomAnimationQueueScreen extends StatefulWidget { - const RoomAnimationQueueScreen({super.key}); - - @override - State createState() => - _RoomAnimationQueueScreenState(); -} - -class _RoomAnimationQueueScreenState extends State { - static const int _maxAnimationQueueLength = 12; - - final List _animationQueue = []; - bool _isQueueProcessing = false; - final Map> _animationKeys = {}; - - @override - void initState() { - super.initState(); - Provider.of(context, listen: false).msgUserJoinListener = - _msgUserJoinListener; - } - - _msgUserJoinListener(Msg msg) { - Future.delayed(Duration(milliseconds: 550), () { - if (!_effectsEnabled) { - return; - } - _addToQueue(msg); - }); - } - - void _addToQueue(Msg msg) { - if (!_effectsEnabled || !mounted) { - return; - } - setState(() { - final taskId = DateTime.now().millisecondsSinceEpoch; - final animationKey = GlobalKey<_RoomEntranceAnimationState>(); - - _trimQueueOverflow(); - _animationKeys[taskId] = animationKey; - - final task = AnimationTask( - id: taskId, - msg: msg, - onComplete: () { - if (_animationQueue.isNotEmpty && - _animationQueue.first.id == taskId) { - setState(() { - _animationQueue.removeAt(0); - _animationKeys.remove(taskId); - }); - - if (_animationQueue.isNotEmpty) { - _startNextAnimation(); - } else { - setState(() => _isQueueProcessing = false); - } - } - }, - ); - - _animationQueue.add(task); - }); - - if (!_isQueueProcessing && _animationQueue.isNotEmpty) { - setState(() => _isQueueProcessing = true); - _startNextAnimation(); - } - } - - void _trimQueueOverflow() { - while (_animationQueue.length >= _maxAnimationQueueLength) { - final removeIndex = - _isQueueProcessing && _animationQueue.length > 1 ? 1 : 0; - final removedTask = _animationQueue.removeAt(removeIndex); - _animationKeys.remove(removedTask.id); - } - } - - void _startNextAnimation({int retryCount = 0}) { - if (_animationQueue.isEmpty) return; - - SchedulerBinding.instance.addPostFrameCallback((_) { - if (_animationQueue.isNotEmpty) { - final task = _animationQueue.first; - final key = _animationKeys[task.id]; - - if (key?.currentState != null) { - key!.currentState!._startAnimation(); - } else if (retryCount < 3) { - // 有限次重试,避免无限循环 - Future.delayed(const Duration(milliseconds: 50), () { - _startNextAnimation(retryCount: retryCount + 1); - }); - } else { - // 重试多次失败后,跳过当前动画 - debugPrint("动画启动失败,跳过当前任务"); - task.onComplete(); - } - } - }); - } - - void _clearQueue() { - if (!mounted) { - _animationQueue.clear(); - _animationKeys.clear(); - _isQueueProcessing = false; - return; - } - setState(() { - _animationQueue.clear(); - _animationKeys.clear(); - _isQueueProcessing = false; - }); - } - - bool get _effectsEnabled => - Provider.of( - context, - listen: false, - ).shouldShowRoomVisualEffects; - - @override - void dispose() { - final rtmProvider = Provider.of(context, listen: false); - if (rtmProvider.msgUserJoinListener == _msgUserJoinListener) { - rtmProvider.msgUserJoinListener = null; - } - _animationQueue.clear(); - _animationKeys.clear(); - _isQueueProcessing = false; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final effectsEnabled = context.select( - (rtcProvider) => rtcProvider.shouldShowRoomVisualEffects, - ); - if (!effectsEnabled) { - if (_animationQueue.isNotEmpty || _isQueueProcessing) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _clearQueue(); - } - }); - } - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - if (_animationQueue.isNotEmpty) - RoomEntranceAnimation( - key: _animationKeys[_animationQueue.first.id], - msg: _animationQueue.first.msg, - onComplete: _animationQueue.first.onComplete, - ), - ], - ), - ], - ), - ); - } -} - -class AnimationTask { - final int id; - final Msg msg; - final VoidCallback onComplete; - - AnimationTask({ - required this.id, - required this.msg, - required this.onComplete, - }); -} - -class RoomEntranceAnimation extends StatefulWidget { - final VoidCallback onComplete; - final Msg msg; - - const RoomEntranceAnimation({ - super.key, - required this.onComplete, - required this.msg, - }); - - @override - State createState() => _RoomEntranceAnimationState(); -} - -class _RoomEntranceAnimationState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _offsetAnimation; - bool _isAnimating = false; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: const Duration(seconds: 4), - vsync: this, - ); - - _offsetAnimation = TweenSequence([ - TweenSequenceItem( - tween: Tween( - begin: const Offset(1.0, 0.0), - end: const Offset(0.0, 0.0), - ).chain(CurveTween(curve: Curves.easeOut)), - weight: 40.0, - ), - TweenSequenceItem( - tween: ConstantTween(const Offset(0.0, 0.0)), - weight: 20.0, - ), - TweenSequenceItem( - tween: Tween( - begin: const Offset(0.0, 0.0), - end: const Offset(-1.0, 0.0), - ).chain(CurveTween(curve: Curves.easeIn)), - weight: 40.0, - ), - ]).animate(_controller); - - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed) { - setState(() => _isAnimating = false); - widget.onComplete(); - } - }); - } - - void _startAnimation() { - if (_isAnimating) return; - - setState(() => _isAnimating = true); - _controller.reset(); - _controller.forward(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SlideTransition( - position: _offsetAnimation, - child: Container( - width: ScreenUtil().screenWidth * 0.8, - height: 37.w, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(getEntranceBg("")), - fit: BoxFit.fill, - ), - ), - child: Row( - textDirection: TextDirection.ltr, - children: [ - Padding( - padding: EdgeInsets.all(2.w), - child: netImage( - url: widget.msg.user?.userAvatar ?? "", - width: 28.w, - height: 28.w, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 5.w), - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 2.w), - socialchatNickNameText( - maxWidth: 150.w, - widget.msg.user?.userNickname ?? "", - fontSize: 12.sp, - type: "", - needScroll: - (widget.msg.user?.userNickname?.characters.length ?? 0) > - 15, - ), - text( - SCAppLocalizations.of(context)!.enterTheRoom, - fontSize: 12.sp, - lineHeight: 0.9, - textColor: Colors.white, - ), - ], - ), - ], - ), - ), - ); - } - - String getEntranceBg(String type) { - return "sc_images/room/entrance/sc_icon_room_entrance_no_vip_bg.png"; - } -} +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/shared/tools/sc_network_image_utils.dart'; +import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; + +class RoomAnimationQueueScreen extends StatefulWidget { + const RoomAnimationQueueScreen({super.key}); + + @override + State createState() => + _RoomAnimationQueueScreenState(); +} + +class _RoomAnimationQueueScreenState extends State { + static const int _maxAnimationQueueLength = 12; + + final List _animationQueue = []; + bool _isQueueProcessing = false; + final Map> _animationKeys = {}; + + @override + void initState() { + super.initState(); + Provider.of(context, listen: false).msgUserJoinListener = + _msgUserJoinListener; + } + + _msgUserJoinListener(Msg msg) { + Future.delayed(Duration(milliseconds: 550), () { + if (!_effectsEnabled) { + return; + } + unawaited( + warmImageResource( + msg.user?.userAvatar ?? "", + logicalWidth: 28.w, + logicalHeight: 28.w, + ), + ); + SCRoomEffectScheduler().scheduleDeferredEffect( + debugLabel: 'room_entrance_join', + action: () { + if (!_effectsEnabled || !mounted) { + return; + } + _addToQueue(msg); + }, + ); + }); + } + + void _addToQueue(Msg msg) { + if (!_effectsEnabled || !mounted) { + return; + } + bool shouldStartProcessing = false; + setState(() { + final taskId = DateTime.now().millisecondsSinceEpoch; + final animationKey = GlobalKey<_RoomEntranceAnimationState>(); + + _trimQueueOverflow(); + _animationKeys[taskId] = animationKey; + + final task = AnimationTask( + id: taskId, + msg: msg, + onComplete: () { + if (_animationQueue.isNotEmpty && + _animationQueue.first.id == taskId) { + setState(() { + _animationQueue.removeAt(0); + _animationKeys.remove(taskId); + }); + + if (_animationQueue.isNotEmpty) { + _startNextAnimation(); + } else { + setState(() => _isQueueProcessing = false); + } + } + }, + ); + + _animationQueue.add(task); + if (!_isQueueProcessing && _animationQueue.isNotEmpty) { + _isQueueProcessing = true; + shouldStartProcessing = true; + } + }); + + if (shouldStartProcessing) { + _startNextAnimation(); + } + } + + void _trimQueueOverflow() { + while (_animationQueue.length >= _maxAnimationQueueLength) { + final removeIndex = + _isQueueProcessing && _animationQueue.length > 1 ? 1 : 0; + final removedTask = _animationQueue.removeAt(removeIndex); + _animationKeys.remove(removedTask.id); + } + } + + void _startNextAnimation({int retryCount = 0}) { + if (_animationQueue.isEmpty) return; + + SchedulerBinding.instance.addPostFrameCallback((_) { + if (_animationQueue.isNotEmpty) { + final task = _animationQueue.first; + final key = _animationKeys[task.id]; + + if (key?.currentState != null) { + key!.currentState!._startAnimation(); + } else if (retryCount < 3) { + // 有限次重试,避免无限循环 + Future.delayed(const Duration(milliseconds: 50), () { + _startNextAnimation(retryCount: retryCount + 1); + }); + } else { + // 重试多次失败后,跳过当前动画 + debugPrint("动画启动失败,跳过当前任务"); + task.onComplete(); + } + } + }); + } + + void _clearQueue() { + if (!mounted) { + _animationQueue.clear(); + _animationKeys.clear(); + _isQueueProcessing = false; + return; + } + setState(() { + _animationQueue.clear(); + _animationKeys.clear(); + _isQueueProcessing = false; + }); + } + + bool get _effectsEnabled => + Provider.of( + context, + listen: false, + ).shouldShowRoomVisualEffects; + + @override + void dispose() { + final rtmProvider = Provider.of(context, listen: false); + if (rtmProvider.msgUserJoinListener == _msgUserJoinListener) { + rtmProvider.msgUserJoinListener = null; + } + _animationQueue.clear(); + _animationKeys.clear(); + _isQueueProcessing = false; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final effectsEnabled = context.select( + (rtcProvider) => rtcProvider.shouldShowRoomVisualEffects, + ); + if (!effectsEnabled) { + if (_animationQueue.isNotEmpty || _isQueueProcessing) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _clearQueue(); + } + }); + } + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + if (_animationQueue.isNotEmpty) + RoomEntranceAnimation( + key: _animationKeys[_animationQueue.first.id], + msg: _animationQueue.first.msg, + onComplete: _animationQueue.first.onComplete, + ), + ], + ), + ], + ), + ); + } +} + +class AnimationTask { + final int id; + final Msg msg; + final VoidCallback onComplete; + + AnimationTask({ + required this.id, + required this.msg, + required this.onComplete, + }); +} + +class RoomEntranceAnimation extends StatefulWidget { + final VoidCallback onComplete; + final Msg msg; + + const RoomEntranceAnimation({ + super.key, + required this.onComplete, + required this.msg, + }); + + @override + State createState() => _RoomEntranceAnimationState(); +} + +class _RoomEntranceAnimationState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _offsetAnimation; + bool _isAnimating = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 4), + vsync: this, + ); + + _offsetAnimation = TweenSequence([ + TweenSequenceItem( + tween: Tween( + begin: const Offset(1.0, 0.0), + end: const Offset(0.0, 0.0), + ).chain(CurveTween(curve: Curves.easeOut)), + weight: 40.0, + ), + TweenSequenceItem( + tween: ConstantTween(const Offset(0.0, 0.0)), + weight: 20.0, + ), + TweenSequenceItem( + tween: Tween( + begin: const Offset(0.0, 0.0), + end: const Offset(-1.0, 0.0), + ).chain(CurveTween(curve: Curves.easeIn)), + weight: 40.0, + ), + ]).animate(_controller); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() => _isAnimating = false); + widget.onComplete(); + } + }); + } + + void _startAnimation() { + if (_isAnimating) return; + + setState(() => _isAnimating = true); + _controller.reset(); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: SlideTransition( + position: _offsetAnimation, + child: Container( + width: ScreenUtil().screenWidth * 0.8, + height: 37.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(getEntranceBg("")), + fit: BoxFit.fill, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + Padding( + padding: EdgeInsets.all(2.w), + child: netImage( + url: widget.msg.user?.userAvatar ?? "", + width: 28.w, + height: 28.w, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 5.w), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 2.w), + socialchatNickNameText( + maxWidth: 150.w, + widget.msg.user?.userNickname ?? "", + fontSize: 12.sp, + type: "", + needScroll: + (widget.msg.user?.userNickname?.characters.length ?? + 0) > + 15, + ), + text( + SCAppLocalizations.of(context)!.enterTheRoom, + fontSize: 12.sp, + lineHeight: 0.9, + textColor: Colors.white, + ), + ], + ), + ], + ), + ), + ), + ); + } + + String getEntranceBg(String type) { + return "sc_images/room/entrance/sc_icon_room_entrance_no_vip_bg.png"; + } +} diff --git a/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart b/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart index 6fa462b..42bfcd6 100644 --- a/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart +++ b/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart @@ -1,5 +1,4 @@ import 'dart:collection'; -import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -212,6 +211,8 @@ class _RoomGiftSeatFlightOverlayState extends State static const int _maxQueuedRequests = 24; final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue(); + final Map> _imageProviderCache = + >{}; final GlobalKey _overlayKey = GlobalKey(); late final AnimationController _controller; @@ -222,6 +223,7 @@ class _RoomGiftSeatFlightOverlayState extends State ImageProvider? _activeImageProvider; Offset? _activeTargetOffset; bool _isPlaying = false; + int _sessionToken = 0; @override void initState() { @@ -254,6 +256,7 @@ class _RoomGiftSeatFlightOverlayState extends State _ensureCenterVisual(request); _trimQueuedRequestsOverflow(); _queue.add(_QueuedRoomGiftSeatFlightRequest(request: request)); + _syncIdleCenterVisual(); _scheduleNextAnimation(); } @@ -278,6 +281,7 @@ class _RoomGiftSeatFlightOverlayState extends State } void _clear() { + _sessionToken += 1; _queue.clear(); _controller.stop(); _controller.reset(); @@ -287,6 +291,7 @@ class _RoomGiftSeatFlightOverlayState extends State _activeRequest = null; _activeImageProvider = null; _activeTargetOffset = null; + _imageProviderCache.clear(); _isPlaying = false; return; } @@ -296,6 +301,7 @@ class _RoomGiftSeatFlightOverlayState extends State _activeRequest = null; _activeImageProvider = null; _activeTargetOffset = null; + _imageProviderCache.clear(); _isPlaying = false; }); } @@ -323,17 +329,7 @@ class _RoomGiftSeatFlightOverlayState extends State for (final queuedRequest in requestsToRemove) { _queue.remove(queuedRequest); } - if (!_isPlaying && _queue.isEmpty) { - if (!mounted) { - _centerRequest = null; - _centerImageProvider = null; - return; - } - setState(() { - _centerRequest = null; - _centerImageProvider = null; - }); - } + _syncIdleCenterVisual(); } int _countTrackedRequests(String queueTag) { @@ -369,16 +365,22 @@ class _RoomGiftSeatFlightOverlayState extends State if (_isPlaying || _queue.isEmpty || !mounted) { return; } + final scheduledToken = _sessionToken; WidgetsBinding.instance.addPostFrameCallback((_) { - _startNextAnimation(); + _startNextAnimation(expectedSessionToken: scheduledToken); }); } - Future _startNextAnimation() async { - if (!mounted || _isPlaying || _queue.isEmpty) { + Future _startNextAnimation({int? expectedSessionToken}) async { + if (!mounted || + _isPlaying || + _queue.isEmpty || + (expectedSessionToken != null && + expectedSessionToken != _sessionToken)) { return; } + final activeSessionToken = _sessionToken; final queuedRequest = _queue.removeFirst(); final targetOffset = _resolveTargetOffset( queuedRequest.request.targetUserId, @@ -386,7 +388,7 @@ class _RoomGiftSeatFlightOverlayState extends State if (targetOffset == null) { if (queuedRequest.retryCount < 6) { Future.delayed(const Duration(milliseconds: 120), () { - if (!mounted) { + if (!mounted || activeSessionToken != _sessionToken) { return; } _queue.addFirst( @@ -395,6 +397,7 @@ class _RoomGiftSeatFlightOverlayState extends State _scheduleNextAnimation(); }); } else { + _syncIdleCenterVisual(); _scheduleNextAnimation(); } return; @@ -421,7 +424,9 @@ class _RoomGiftSeatFlightOverlayState extends State ).timeout(const Duration(milliseconds: 250)); } catch (_) {} - if (!mounted || _activeRequest != queuedRequest.request) { + if (!mounted || + activeSessionToken != _sessionToken || + _activeRequest != queuedRequest.request) { return; } @@ -430,28 +435,21 @@ class _RoomGiftSeatFlightOverlayState extends State } void _finishCurrentAnimation() { - final hasPendingRequests = _queue.isNotEmpty; if (!mounted) { - if (!hasPendingRequests) { - _centerRequest = null; - _centerImageProvider = null; - } _activeRequest = null; _activeImageProvider = null; _activeTargetOffset = null; _isPlaying = false; + _syncIdleCenterVisual(); return; } setState(() { - if (!hasPendingRequests) { - _centerRequest = null; - _centerImageProvider = null; - } _activeRequest = null; _activeImageProvider = null; _activeTargetOffset = null; _isPlaying = false; }); + _syncIdleCenterVisual(); _scheduleNextAnimation(); } @@ -459,7 +457,10 @@ class _RoomGiftSeatFlightOverlayState extends State if (_centerRequest != null && _centerImageProvider != null) { return; } - final imageProvider = _resolveImageProvider(request.imagePath); + final imageProvider = _resolveImageProvider( + request.imagePath, + request: request, + ); if (!mounted) { _centerRequest = request; _centerImageProvider = imageProvider; @@ -471,6 +472,39 @@ class _RoomGiftSeatFlightOverlayState extends State }); } + void _syncIdleCenterVisual() { + if (_isPlaying || _activeRequest != null) { + return; + } + + final nextRequest = _queue.isNotEmpty ? _queue.first.request : null; + final nextImageProvider = + nextRequest == null + ? null + : _resolveImageProvider( + nextRequest.imagePath, + request: nextRequest, + ); + + if (!mounted) { + _centerRequest = nextRequest; + _centerImageProvider = nextImageProvider; + return; + } + + final shouldUpdate = + _centerRequest != nextRequest || + _centerImageProvider != nextImageProvider; + if (!shouldUpdate) { + return; + } + + setState(() { + _centerRequest = nextRequest; + _centerImageProvider = nextImageProvider; + }); + } + Offset? _resolveTargetOffset(String targetUserId) { final overlayContext = _overlayKey.currentContext; final targetKey = widget.resolveTargetKey(targetUserId); @@ -494,18 +528,34 @@ class _RoomGiftSeatFlightOverlayState extends State return overlayBox.globalToLocal(globalTargetCenter); } - ImageProvider _resolveImageProvider(String imagePath) { - if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) { - return buildCachedImageProvider(imagePath); + ImageProvider _resolveImageProvider( + String imagePath, { + RoomGiftSeatFlightRequest? request, + }) { + final currentRequest = request ?? _activeRequest ?? _centerRequest; + final logicalSize = + currentRequest == null + ? null + : currentRequest.beginSize > currentRequest.endSize + ? currentRequest.beginSize + : currentRequest.endSize; + final cacheKey = '$imagePath|${logicalSize?.toStringAsFixed(2) ?? "auto"}'; + final cachedProvider = _imageProviderCache[cacheKey]; + if (cachedProvider != null) { + return cachedProvider; } - if (imagePath.startsWith('/')) { - return FileImage(File(imagePath)); - } - return AssetImage(imagePath); + final provider = buildCachedImageProvider( + imagePath, + logicalWidth: logicalSize, + logicalHeight: logicalSize, + ); + _imageProviderCache[cacheKey] = provider; + return provider; } @override Widget build(BuildContext context) { + final overlaySize = MediaQuery.sizeOf(context); return IgnorePointer( child: SizedBox.expand( key: _overlayKey, @@ -518,11 +568,6 @@ class _RoomGiftSeatFlightOverlayState extends State : AnimatedBuilder( animation: _controller, builder: (context, child) { - final renderBox = - _overlayKey.currentContext?.findRenderObject() - as RenderBox?; - final overlaySize = - renderBox?.size ?? MediaQuery.sizeOf(context); final center = overlaySize.center(Offset.zero); final currentCenterRequest = _centerRequest ?? _activeRequest; @@ -576,7 +621,9 @@ class _RoomGiftSeatFlightOverlayState extends State ); } - return Stack(clipBehavior: Clip.none, children: children); + return RepaintBoundary( + child: Stack(clipBehavior: Clip.none, children: children), + ); }, ), ), diff --git a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart index d1a311e..68858da 100644 --- a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart +++ b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; class LuckGiftNomorAnimWidget extends StatefulWidget { @@ -17,6 +18,20 @@ class LuckGiftNomorAnimWidget extends StatefulWidget { } class _LuckGiftNomorAnimWidgetState extends State { + static const double _groupShiftXRatio = 26 / 422; + static const double _groupShiftYRatio = 18 / 422; + static const double _groupShiftYExtraUnits = 40; + static const double _avatarScale = 0.8; + static const double _avatarExtraShiftXUnits = -2; + static const double _avatarExtraShiftYUnits = 18; + static const double _avatarDiameterRatio = 76 / 422; + static const double _avatarLeftRatio = 152 / 422; + static const double _avatarTopRatio = 64 / 422; + static const double _awardTopRatio = 153 / 422; + static const double _awardXOffsetRatio = -13 / 422; + static const double _multipleTopRatio = 193 / 422; + static const double _multipleXOffsetRatio = -22 / 422; + String? _currentBurstEventId; bool _isRewardAmountVisible = false; @@ -34,6 +49,48 @@ class _LuckGiftNomorAnimWidgetState extends State { return awardAmount.toString(); } + String _formatMultiple(num multiple) { + if (multiple % 1 == 0) { + return multiple.toInt().toString(); + } + return multiple.toString(); + } + + Widget _buildSenderAvatar({ + required String avatarUrl, + required double outerSize, + }) { + final avatarPadding = outerSize * 0.045; + final innerSize = outerSize - avatarPadding * 2; + return RepaintBoundary( + child: Container( + width: outerSize, + height: outerSize, + padding: EdgeInsets.all(avatarPadding), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFFFD680), + boxShadow: const [ + BoxShadow( + color: Color(0x55243A74), + blurRadius: 16, + offset: Offset(0, 6), + ), + ], + ), + child: ClipOval( + child: netImage( + url: avatarUrl, + width: innerSize, + height: innerSize, + fit: BoxFit.cover, + shape: BoxShape.circle, + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { return IgnorePointer( @@ -45,6 +102,28 @@ class _LuckGiftNomorAnimWidgetState extends State { _isRewardAmountVisible = false; return const SizedBox.shrink(); } + final screenWidth = ScreenUtil().screenWidth; + final groupShiftX = screenWidth * _groupShiftXRatio; + final groupShiftY = + screenWidth * _groupShiftYRatio + _groupShiftYExtraUnits.w; + final avatarBaseDiameter = screenWidth * _avatarDiameterRatio; + final avatarDiameter = avatarBaseDiameter * _avatarScale; + final avatarInset = (avatarBaseDiameter - avatarDiameter) / 2; + final avatarLeft = + screenWidth * _avatarLeftRatio + + groupShiftX + + avatarInset + + _avatarExtraShiftXUnits.w; + final avatarTop = + screenWidth * _avatarTopRatio + + groupShiftY + + avatarInset + + _avatarExtraShiftYUnits.w; + final awardTop = screenWidth * _awardTopRatio + groupShiftY; + final awardXOffset = screenWidth * _awardXOffsetRatio + groupShiftX; + final multipleTop = screenWidth * _multipleTopRatio + groupShiftY; + final multipleXOffset = + screenWidth * _multipleXOffsetRatio + groupShiftX; final rewardAnimationKey = ValueKey( '${rewardData.sendUserId ?? ""}' '|${rewardData.acceptUserId ?? ""}' @@ -93,37 +172,83 @@ class _LuckGiftNomorAnimWidgetState extends State { }, ), ), + if (_isRewardAmountVisible && + (rewardData.userAvatar ?? '').trim().isNotEmpty) + Positioned( + left: avatarLeft, + top: avatarTop, + child: _buildSenderAvatar( + avatarUrl: (rewardData.userAvatar ?? '').trim(), + outerSize: avatarDiameter, + ), + ), if (_isRewardAmountVisible) - Positioned.fill( - child: Align( - alignment: const Alignment(0, 0.12), - child: Transform.translate( - offset: Offset(0, 0), + Positioned( + top: awardTop, + left: 0, + right: 0, + child: Transform.translate( + offset: Offset(awardXOffset, 0), + child: Center( child: ConstrainedBox( constraints: BoxConstraints( - maxWidth: ScreenUtil().screenWidth * 0.56, + maxWidth: screenWidth * 0.34, ), child: FittedBox( fit: BoxFit.scaleDown, - child: Padding( - padding: EdgeInsets.only(left: 6.w, right: 2.w), - child: Text( - _formatAwardAmount(rewardData.awardAmount ?? 0), - maxLines: 1, - style: TextStyle( - fontSize: 28.sp, - color: const Color(0xFFFFF3B6), - fontWeight: FontWeight.w900, - fontStyle: FontStyle.italic, - height: 1, - shadows: const [ - Shadow( - color: Color(0xCC7A3E00), - blurRadius: 12, - offset: Offset(0, 3), - ), - ], - ), + child: Text( + _formatAwardAmount(rewardData.awardAmount ?? 0), + maxLines: 1, + style: TextStyle( + fontSize: 28.sp, + color: const Color(0xFFFFF3B6), + fontWeight: FontWeight.w900, + fontStyle: FontStyle.italic, + height: 1, + shadows: const [ + Shadow( + color: Color(0xCC7A3E00), + blurRadius: 12, + offset: Offset(0, 3), + ), + ], + ), + ), + ), + ), + ), + ), + ), + if (_isRewardAmountVisible && (rewardData.multiple ?? 0) > 0) + Positioned( + top: multipleTop, + left: 0, + right: 0, + child: Transform.translate( + offset: Offset(multipleXOffset, 0), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: screenWidth * 0.18, + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'x${_formatMultiple(rewardData.multiple ?? 0)}', + maxLines: 1, + style: TextStyle( + fontSize: 17.sp, + color: const Color(0xFFF4F6FF), + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + height: 1, + shadows: const [ + Shadow( + color: Color(0x990B2D72), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], ), ), ), diff --git a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart index 900fb41..389825b 100644 --- a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart @@ -1,483 +1,532 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_debouncer/flutter_debouncer.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/tools/sc_room_utils.dart'; -import 'package:yumi/main.dart'; -import 'package:marquee/marquee.dart'; - -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; - -class FloatingLuckGiftScreenWidget extends StatefulWidget { - final SCFloatingMessage message; - final VoidCallback onAnimationCompleted; // 动画完成回调 - - const FloatingLuckGiftScreenWidget({ - Key? key, - required this.message, - required this.onAnimationCompleted, - }) : super(key: key); - - @override - _FloatingLuckGiftScreenWidgetState createState() => - _FloatingLuckGiftScreenWidgetState(); -} - -class _FloatingLuckGiftScreenWidgetState - extends State - with TickerProviderStateMixin { - static const String _coinIconAssetPath = "sc_images/general/sc_icon_jb.png"; - late AnimationController _controller; - late Animation _offsetAnimation; - late AnimationController _swipeController; // 新增:滑动动画控制器 - late Animation _swipeAnimation; // 新增:滑动动画 - Debouncer debouncer = Debouncer(); - - bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 - - @override - void initState() { - super.initState(); - - // 主动画控制器 - _controller = AnimationController( - duration: const Duration(seconds: 5), - vsync: this, - ); - - // 滑动动画控制器 - _swipeController = AnimationController( - duration: const Duration(milliseconds: 550), // 滑动动画500ms - vsync: this, - ); - - // 监听滑动动画完成 - _swipeController.addStatusListener((status) { - if (status == AnimationStatus.completed) { - widget.onAnimationCompleted(); - } - }); - - // 主动画:从右向左移动 - _offsetAnimation = Tween( - begin: const Offset(1.0, 0.0), - end: const Offset(-1.0, 0.0), - ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); - - // 滑动动画:从当前位置快速向左滑出 - _swipeAnimation = Tween( - begin: Offset.zero, // 从当前位置开始 - end: const Offset(-1.5, 0.0), // 到屏幕左侧外 - ).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn)); - - // 监听主动画完成 - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed && !_isSwipeAnimating) { - widget.onAnimationCompleted(); - } - }); - - // 延迟启动动画,确保组件已经完全构建 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _controller.forward(); - } - }); - } - - // 处理向左滑动 - void _handleSwipeLeft() { - if (_isSwipeAnimating) return; - - setState(() { - _isSwipeAnimating = true; - }); - - // 停止主动画 - _controller.stop(); - - // 启动滑动动画 - _swipeController.reset(); - _swipeController.forward(); - } - - @override - void dispose() { - _controller.dispose(); - _swipeController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return _buildLuckGiftAnimation(); - } - - ///幸运礼物飘屏 - Widget _buildLuckGiftAnimation() { - return GestureDetector( - onTap: () { - debouncer.debounce( - duration: Duration(milliseconds: 350), - onDebounce: () { - if (widget.message.roomId != null && - widget.message.roomId!.isNotEmpty) { - SCRoomUtils.goRoom( - widget.message.roomId!, - navigatorKey.currentState!.context, - fromFloting: true, - ); - } - }, - ); - }, - onHorizontalDragEnd: (details) { - // 获取当前的文本方向 - final textDirection = Directionality.of(context); - - // 定义一个根据方向转换速度符号的辅助函数 - double effectiveVelocity(double velocity) { - // 在RTL模式下,反转速度的正负号 - return textDirection == TextDirection.rtl ? -velocity : velocity; - } - - double velocity = effectiveVelocity(details.primaryVelocity ?? 0); - if (velocity < 0) { - // 向左滑动,把当前视图向左平移出去 - _handleSwipeLeft(); - } - }, - child: SlideTransition( - position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, - child: Container( - alignment: Alignment.center, - height: 83.w, - width: 350.w, - child: Stack( - children: [ - Transform.flip( - flipX: SCGlobalConfig.lang == "ar" ? true : false, // 水平翻转 - flipY: false, // 垂直翻转设为 false - child: Image.asset( - "sc_images/room/sc_icon_luck_gift_float_n_bg.png", - fit: BoxFit.fill, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, // 宽度由内容决定 - children: [ - Container( - margin: EdgeInsetsDirectional.only(top: 14.w, start: 1.w), - child: netImage( - url: widget.message.userAvatarUrl ?? "", - width: 52.w, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 4.w), - Expanded( - child: Container( - margin: EdgeInsets.only(top: 10.w), - child: Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: 85.w, - maxHeight: 20.w, - ), - child: - (widget.message.userName?.length ?? 0) > 6 - ? Marquee( - text: "${widget.message.userName} ", - style: TextStyle( - fontSize: 13.sp, - color: Color(0xffFEF129), - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - letterSpacing: 0.1, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: - CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration( - seconds: 1, - ), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - "${widget.message.userName} ", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Color(0xffFEF129), - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - SizedBox(width: 3.w), - Expanded(child: _buildRewardLine(context)), - SizedBox(width: 6.w), - Container( - width: 80.w, - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 2.w), - buildNumForGame( - "${widget.message.multiple ?? 0}", - size: 25.w, - ), - SizedBox(height: 3.w), - SCGlobalConfig.lang == "ar" - ? Image.asset( - "sc_images/room/sc_icon_times_text_ar.png", - height: 12.w, - ) - : Image.asset( - "sc_images/room/sc_icon_times_text_en.png", - height: 12.w, - ), - ], - ), - ), - SizedBox(width: 6.w), - ], - ), - ), - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildRewardLine(BuildContext context) { - final baseStyle = TextStyle( - fontSize: 12.sp, - color: Colors.white, - fontWeight: FontWeight.bold, - letterSpacing: 0.1, - decoration: TextDecoration.none, - ); - final amountStyle = baseStyle.copyWith(color: const Color(0xffFEF129)); - return Text.rich( - TextSpan( - children: [ - TextSpan(text: SCAppLocalizations.of(context)!.get, style: baseStyle), - TextSpan( - text: " ${_formatCoins(widget.message.coins)} ", - style: amountStyle, - ), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Padding( - padding: EdgeInsetsDirectional.only(end: 3.w), - child: Image.asset(_coinIconAssetPath, width: 16.w, height: 16.w), - ), - ), - TextSpan(text: "from ", style: baseStyle), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: netImage( - url: widget.message.giftUrl ?? "", - width: 18.w, - height: 18.w, - ), - ), - ], - ), - textAlign: TextAlign.start, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - strutStyle: StrutStyle( - height: 1.1, - fontWeight: FontWeight.bold, - forceStrutHeight: true, - ), - ); - } - - String _formatCoins(num? coins) { - final value = (coins ?? 0); - if (value > 9999) { - return "${(value / 1000).toStringAsFixed(0)}k"; - } - if (value % 1 == 0) { - return value.toInt().toString(); - } - return value.toString(); - } - - ///礼物总价钱达到10000 - Widget _buildGiftAnimation() { - return GestureDetector( - onTap: () { - debouncer.debounce( - duration: Duration(milliseconds: 350), - onDebounce: () { - if (widget.message.roomId != null && - widget.message.roomId!.isNotEmpty) { - SCRoomUtils.goRoom( - widget.message.roomId!, - navigatorKey.currentState!.context, - fromFloting: true, - ); - } - }, - ); - }, - onHorizontalDragEnd: (details) { - // 获取当前的文本方向 - final textDirection = Directionality.of(context); - - // 定义一个根据方向转换速度符号的辅助函数 - double effectiveVelocity(double velocity) { - // 在RTL模式下,反转速度的正负号 - return textDirection == TextDirection.rtl ? -velocity : velocity; - } - - double velocity = effectiveVelocity(details.primaryVelocity ?? 0); - if (velocity < 0) { - // 向左滑动,把当前视图向左平移出去 - _handleSwipeLeft(); - } - }, - child: SlideTransition( - position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, - child: Container( - alignment: Alignment.center, - height: 50.w, - width: 290.w, - margin: EdgeInsets.only(top: 20.w), - padding: EdgeInsets.symmetric(horizontal: 15.w), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage("sc_images/room/sc_icon_gift_float_bg.png"), - fit: BoxFit.fill, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, // 宽度由内容决定 - children: [ - // 可以根据消息模型丰富内容,例如显示头像 - if (widget.message.userAvatarUrl?.isNotEmpty ?? false) - head(url: widget.message.userAvatarUrl ?? "", width: 42.w), - SizedBox(width: 4.w), - Expanded( - child: Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: 48.w, - maxHeight: 21.w, - ), - child: - (widget.message.userName?.length ?? 0) > 6 - ? Marquee( - text: widget.message.userName ?? "", - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - widget.message.userName ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - text( - SCAppLocalizations.of(context)!.sendTo, - textColor: Colors.white, - fontSize: 13.sp, - fontWeight: FontWeight.bold, - ), - Container( - constraints: BoxConstraints( - maxWidth: 48.w, - maxHeight: 21.w, - ), - child: - (widget.message.toUserName?.length ?? 0) > 6 - ? Marquee( - text: widget.message.toUserName ?? "", - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - widget.message.toUserName ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - ], - ), - ), - netImage(url: widget.message.giftUrl ?? "", width: 32.w), - SizedBox(width: 3.w), - Image.asset("sc_images/room/sc_icon_x.png", width: 18.w), - buildNum("${widget.message.number}", size: 18.w), - ], - ), - ), - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/main.dart'; +import 'package:marquee/marquee.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +class FloatingLuckGiftScreenWidget extends StatefulWidget { + final SCFloatingMessage message; + final VoidCallback onAnimationCompleted; // 动画完成回调 + + const FloatingLuckGiftScreenWidget({ + Key? key, + required this.message, + required this.onAnimationCompleted, + }) : super(key: key); + + @override + _FloatingLuckGiftScreenWidgetState createState() => + _FloatingLuckGiftScreenWidgetState(); +} + +class _FloatingLuckGiftScreenWidgetState + extends State + with TickerProviderStateMixin { + static const String _coinIconAssetPath = "sc_images/general/sc_icon_jb.png"; + late AnimationController _controller; + late Animation _offsetAnimation; + late AnimationController _swipeController; // 新增:滑动动画控制器 + late Animation _swipeAnimation; // 新增:滑动动画 + Debouncer debouncer = Debouncer(); + + bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 + + @override + void initState() { + super.initState(); + + // 主动画控制器 + _controller = AnimationController( + duration: const Duration(seconds: 5), + vsync: this, + ); + + // 滑动动画控制器 + _swipeController = AnimationController( + duration: const Duration(milliseconds: 550), // 滑动动画500ms + vsync: this, + ); + + // 监听滑动动画完成 + _swipeController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onAnimationCompleted(); + } + }); + + // 主动画:从右向左移动 + _offsetAnimation = Tween( + begin: const Offset(1.0, 0.0), + end: const Offset(-1.0, 0.0), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + + // 滑动动画:从当前位置快速向左滑出 + _swipeAnimation = Tween( + begin: Offset.zero, // 从当前位置开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn)); + + // 监听主动画完成 + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed && !_isSwipeAnimating) { + widget.onAnimationCompleted(); + } + }); + + // 延迟启动动画,确保组件已经完全构建 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.forward(); + } + }); + } + + // 处理向左滑动 + void _handleSwipeLeft() { + if (_isSwipeAnimating) return; + + setState(() { + _isSwipeAnimating = true; + }); + + // 停止主动画 + _controller.stop(); + + // 启动滑动动画 + _swipeController.reset(); + _swipeController.forward(); + } + + @override + void dispose() { + _controller.dispose(); + _swipeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _buildLuckGiftAnimation(); + } + + ///幸运礼物飘屏 + Widget _buildLuckGiftAnimation() { + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true, + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl ? -velocity : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + // 向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: SlideTransition( + position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, + child: Container( + alignment: Alignment.center, + height: 83.w, + width: 350.w, + child: Stack( + children: [ + Transform.flip( + flipX: SCGlobalConfig.lang == "ar" ? true : false, // 水平翻转 + flipY: false, // 垂直翻转设为 false + child: Image.asset( + "sc_images/room/sc_icon_luck_gift_float_n_bg.png", + fit: BoxFit.fill, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + Container( + margin: EdgeInsetsDirectional.only(top: 14.w, start: 1.w), + child: netImage( + url: widget.message.userAvatarUrl ?? "", + width: 52.w, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.w), + child: Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: 85.w, + maxHeight: 20.w, + ), + child: + (widget.message.userName?.length ?? 0) > 6 + ? Marquee( + text: "${widget.message.userName} ", + style: TextStyle( + fontSize: 13.sp, + color: Color(0xffFEF129), + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + letterSpacing: 0.1, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + "${widget.message.userName} ", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Color(0xffFEF129), + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + SizedBox(width: 3.w), + Expanded(child: _buildRewardLine(context)), + SizedBox(width: 6.w), + Container( + width: 80.w, + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 2.w), + buildNumForGame( + "${widget.message.multiple ?? 0}", + size: 25.w, + ), + SizedBox(height: 3.w), + SCGlobalConfig.lang == "ar" + ? Image.asset( + "sc_images/room/sc_icon_times_text_ar.png", + height: 12.w, + ) + : Image.asset( + "sc_images/room/sc_icon_times_text_en.png", + height: 12.w, + ), + ], + ), + ), + SizedBox(width: 6.w), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildRewardLine(BuildContext context) { + final baseStyle = TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.1, + decoration: TextDecoration.none, + ); + final amountStyle = baseStyle.copyWith(color: const Color(0xffFEF129)); + return Text.rich( + TextSpan( + children: [ + TextSpan(text: SCAppLocalizations.of(context)!.get, style: baseStyle), + TextSpan( + text: " ${_formatCoins(widget.message.coins)} ", + style: amountStyle, + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: EdgeInsetsDirectional.only(end: 3.w), + child: Image.asset(_coinIconAssetPath, width: 16.w, height: 16.w), + ), + ), + TextSpan(text: "from ", style: baseStyle), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: _buildGiftIcon(context), + ), + ], + ), + textAlign: TextAlign.start, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + strutStyle: StrutStyle( + height: 1.1, + fontWeight: FontWeight.bold, + forceStrutHeight: true, + ), + ); + } + + Widget _buildGiftIcon(BuildContext context) { + final primaryGiftUrl = (widget.message.giftUrl ?? "").trim(); + final fallbackGiftUrl = _resolveFallbackGiftUrl(context); + final displayGiftUrl = + primaryGiftUrl.isNotEmpty ? primaryGiftUrl : fallbackGiftUrl; + if (displayGiftUrl.isEmpty) { + return _buildGiftIconPlaceholder(); + } + + final backupUrl = + fallbackGiftUrl.isNotEmpty && fallbackGiftUrl != displayGiftUrl + ? fallbackGiftUrl + : ""; + + return netImage( + url: displayGiftUrl, + width: 18.w, + height: 18.w, + borderRadius: BorderRadius.circular(3.w), + loadingWidget: _buildGiftIconPlaceholder(), + errorWidget: + backupUrl.isNotEmpty + ? netImage( + url: backupUrl, + width: 18.w, + height: 18.w, + borderRadius: BorderRadius.circular(3.w), + noDefaultImg: true, + loadingWidget: _buildGiftIconPlaceholder(), + errorWidget: _buildGiftIconPlaceholder(), + ) + : _buildGiftIconPlaceholder(), + ); + } + + String _resolveFallbackGiftUrl(BuildContext context) { + final giftId = (widget.message.giftId ?? "").trim(); + if (giftId.isEmpty) { + return ""; + } + final gift = Provider.of( + context, + listen: false, + ).getGiftById(giftId); + return (gift?.giftPhoto ?? "").trim(); + } + + Widget _buildGiftIconPlaceholder() { + return SizedBox(width: 18.w, height: 18.w); + } + + String _formatCoins(num? coins) { + final value = (coins ?? 0); + if (value > 9999) { + return "${(value / 1000).toStringAsFixed(0)}k"; + } + if (value % 1 == 0) { + return value.toInt().toString(); + } + return value.toString(); + } + + ///礼物总价钱达到10000 + Widget _buildGiftAnimation() { + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true, + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl ? -velocity : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + // 向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: SlideTransition( + position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, + child: Container( + alignment: Alignment.center, + height: 50.w, + width: 290.w, + margin: EdgeInsets.only(top: 20.w), + padding: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_gift_float_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + // 可以根据消息模型丰富内容,例如显示头像 + if (widget.message.userAvatarUrl?.isNotEmpty ?? false) + head(url: widget.message.userAvatarUrl ?? "", width: 42.w), + SizedBox(width: 4.w), + Expanded( + child: Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: 48.w, + maxHeight: 21.w, + ), + child: + (widget.message.userName?.length ?? 0) > 6 + ? Marquee( + text: widget.message.userName ?? "", + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + widget.message.userName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + text( + SCAppLocalizations.of(context)!.sendTo, + textColor: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.bold, + ), + Container( + constraints: BoxConstraints( + maxWidth: 48.w, + maxHeight: 21.w, + ), + child: + (widget.message.toUserName?.length ?? 0) > 6 + ? Marquee( + text: widget.message.toUserName ?? "", + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + widget.message.toUserName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ), + netImage(url: widget.message.giftUrl ?? "", width: 32.w), + SizedBox(width: 3.w), + Image.asset("sc_images/room/sc_icon_x.png", width: 18.w), + buildNum("${widget.message.number}", size: 18.w), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_bottom_widget.dart b/lib/ui_kit/widgets/room/room_bottom_widget.dart index 48a83ef..008d831 100644 --- a/lib/ui_kit/widgets/room/room_bottom_widget.dart +++ b/lib/ui_kit/widgets/room/room_bottom_widget.dart @@ -277,9 +277,11 @@ class _RoomBottomWidgetState extends State { provider.isMic = !provider.isMic; provider.roomWheatMap.forEach((k, v) { - if (v.user?.id == - AccountStorage().getCurrentUser()?.userProfile?.id && - !provider.roomWheatMap[k]!.micMute!) { + final seat = provider.micAtIndexForDisplay(k); + if ((seat?.user?.id ?? "").trim() == + (AccountStorage().getCurrentUser()?.userProfile?.id ?? "") + .trim() && + !(seat?.micMute ?? true)) { if (!provider.isMic) { provider.engine?.adjustRecordingSignalVolume(100); provider.engine?.setClientRole( @@ -313,15 +315,7 @@ class _RoomBottomWidgetState extends State { } bool _shouldShowMic(RtcProvider provider) { - var show = false; - - provider.roomWheatMap.forEach((k, v) { - if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { - show = true; - } - }); - - return show; + return provider.isOnMai(); } } 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 811160b..c0c87e3 100644 --- a/lib/ui_kit/widgets/room/room_user_info_card.dart +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -26,6 +26,7 @@ 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'; import 'package:yumi/modules/gift/gift_page.dart'; +import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart'; import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; @@ -265,11 +266,40 @@ class _RoomUserInfoCardState extends State { ], ), SizedBox(height: 2.w), - text( - "ID:${ref.userCardInfo?.userProfile?.getID() ?? 0}", - fontSize: 12.sp, - textColor: Colors.black, - fontWeight: FontWeight.bold, + SCSpecialIdBadge( + idText: + ref + .userCardInfo + ?.userProfile + ?.getID() ?? + "", + showAnimated: + ref + .userCardInfo + ?.userProfile + ?.hasSpecialId() ?? + false, + assetPath: + SCSpecialIdAssets.userId, + animationWidth: 110.w, + animationHeight: 28.w, + textPadding: + EdgeInsets.fromLTRB( + 33.w, + 6.w, + 16.w, + 6.w, + ), + animationTextStyle: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + normalTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), ), ], ), diff --git a/pubspec.lock b/pubspec.lock index 31f9492..7e25cb1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _flutterfire_internals sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.59" agora_rtc_engine: @@ -14,7 +14,7 @@ packages: description: name: agora_rtc_engine sha256: "6559294d18ce4445420e19dbdba10fb58cac955cd8f22dbceae26716e194d70e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.5.3" app_links: @@ -22,7 +22,7 @@ packages: description: name: app_links sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.1" app_links_linux: @@ -30,7 +30,7 @@ packages: description: name: app_links_linux sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" app_links_platform_interface: @@ -38,7 +38,7 @@ packages: description: name: app_links_platform_interface sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" app_links_web: @@ -46,7 +46,7 @@ packages: description: name: app_links_web sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.4" archive: @@ -54,7 +54,7 @@ packages: description: name: archive sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.9" args: @@ -62,7 +62,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" async: @@ -70,7 +70,7 @@ packages: description: name: async sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.13.1" audioplayers: @@ -78,7 +78,7 @@ packages: description: name: audioplayers sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.6.0" audioplayers_android: @@ -86,7 +86,7 @@ packages: description: name: audioplayers_android sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.2.1" audioplayers_darwin: @@ -94,7 +94,7 @@ packages: description: name: audioplayers_darwin sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.0" audioplayers_linux: @@ -102,7 +102,7 @@ packages: description: name: audioplayers_linux sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.1" audioplayers_platform_interface: @@ -110,7 +110,7 @@ packages: description: name: audioplayers_platform_interface sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.1.1" audioplayers_web: @@ -118,7 +118,7 @@ packages: description: name: audioplayers_web sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.2.0" audioplayers_windows: @@ -126,7 +126,7 @@ packages: description: name: audioplayers_windows sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.0" back_button_interceptor: @@ -134,23 +134,23 @@ packages: description: name: back_button_interceptor sha256: b85977faabf4aeb95164b3b8bf81784bed4c54ea1aef90a036ab6927ecf80c5a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.0.4" badges: dependency: "direct main" description: name: badges - sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 - url: "https://pub.dev" + sha256: cf1c88fb3777df69ccd630b80de5267f54efa4a39381b0404a7c03d56cb7c041 + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.2" + version: "3.2.0" boolean_selector: dependency: transitive description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" cached_network_image: @@ -158,7 +158,7 @@ packages: description: name: cached_network_image sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.4.1" cached_network_image_platform_interface: @@ -166,7 +166,7 @@ packages: description: name: cached_network_image_platform_interface sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1" cached_network_image_web: @@ -174,7 +174,7 @@ packages: description: name: cached_network_image_web sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" carousel_slider: @@ -182,7 +182,7 @@ packages: description: name: carousel_slider sha256: febf4b0163e0242adc13d7a863b04965351f59e7dfea56675c7c2caa7bcd7476 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.2" characters: @@ -190,7 +190,7 @@ packages: description: name: characters sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" checked_yaml: @@ -198,7 +198,7 @@ packages: description: name: checked_yaml sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" cli_util: @@ -206,7 +206,7 @@ packages: description: name: cli_util sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.2" clock: @@ -214,7 +214,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" code_assets: @@ -222,7 +222,7 @@ packages: description: name: code_assets sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" collection: @@ -230,7 +230,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" cookie_jar: @@ -238,7 +238,7 @@ packages: description: name: cookie_jar sha256: "963da02c1ef64cb5ac20de948c9e5940aa351f1e34a12b1d327c83d85b7e8fff" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.9" cross_file: @@ -246,7 +246,7 @@ packages: description: name: cross_file sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.5+2" crypto: @@ -254,7 +254,7 @@ packages: description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.7" csslib: @@ -262,7 +262,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" cupertino_icons: @@ -270,7 +270,7 @@ packages: description: name: cupertino_icons sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.9" dbus: @@ -278,7 +278,7 @@ packages: description: name: dbus sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.12" device_info_plus: @@ -286,7 +286,7 @@ packages: description: name: device_info_plus sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.1.2" device_info_plus_platform_interface: @@ -294,7 +294,7 @@ packages: description: name: device_info_plus_platform_interface sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.3" dio: @@ -302,7 +302,7 @@ packages: description: name: dio sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.9.2" dio_web_adapter: @@ -310,7 +310,7 @@ packages: description: name: dio_web_adapter sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" event_bus: @@ -318,7 +318,7 @@ packages: description: name: event_bus sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" extended_image: @@ -326,7 +326,7 @@ packages: description: name: extended_image sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.0.1" extended_image_library: @@ -334,7 +334,7 @@ packages: description: name: extended_image_library sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.1" extended_nested_scroll_view: @@ -342,7 +342,7 @@ packages: description: name: extended_nested_scroll_view sha256: "835580d40c2c62b448bd14adecd316acba469ba61f1510ef559d17668a85e777" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.2.1" extended_text: @@ -350,7 +350,7 @@ packages: description: name: extended_text sha256: d8f4a6e2676505b54dc0d5f5e8de9020667b402e9c1b3a8b030a83e568c99654 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "15.0.2" extended_text_field: @@ -358,7 +358,7 @@ packages: description: name: extended_text_field sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "16.0.2" extended_text_library: @@ -366,7 +366,7 @@ packages: description: name: extended_text_library sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "12.0.1" fading_edge_scrollview: @@ -374,7 +374,7 @@ packages: description: name: fading_edge_scrollview sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1" fake_async: @@ -382,7 +382,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" ffi: @@ -390,7 +390,7 @@ packages: description: name: ffi sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" file: @@ -398,7 +398,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" file_selector_linux: @@ -406,7 +406,7 @@ packages: description: name: file_selector_linux sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.4" file_selector_macos: @@ -414,7 +414,7 @@ packages: description: name: file_selector_macos sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.5" file_selector_platform_interface: @@ -422,7 +422,7 @@ packages: description: name: file_selector_platform_interface sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" file_selector_windows: @@ -430,7 +430,7 @@ packages: description: name: file_selector_windows sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3+5" firebase_auth: @@ -438,7 +438,7 @@ packages: description: name: firebase_auth sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.7.0" firebase_auth_platform_interface: @@ -446,7 +446,7 @@ packages: description: name: firebase_auth_platform_interface sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.7.3" firebase_auth_web: @@ -454,7 +454,7 @@ packages: description: name: firebase_auth_web sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.15.3" firebase_core: @@ -462,7 +462,7 @@ packages: description: name: firebase_core sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.15.2" firebase_core_platform_interface: @@ -470,7 +470,7 @@ packages: description: name: firebase_core_platform_interface sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" firebase_core_web: @@ -478,7 +478,7 @@ packages: description: name: firebase_core_web sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.24.1" firebase_crashlytics: @@ -486,7 +486,7 @@ packages: description: name: firebase_crashlytics sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.10" firebase_crashlytics_platform_interface: @@ -494,7 +494,7 @@ packages: description: name: firebase_crashlytics_platform_interface sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.8.10" fixnum: @@ -502,7 +502,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" fluro: @@ -510,7 +510,7 @@ packages: description: name: fluro sha256: "24d07d0b285b213ec2045b83e85d076185fa5c23651e44dae0ac6755784b97d0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.5" flutter: @@ -523,7 +523,7 @@ packages: description: name: flutter_cache_manager sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.4.1" flutter_debouncer: @@ -531,7 +531,7 @@ packages: description: name: flutter_debouncer sha256: "89f98f874e6abbb212f3027a7a27d5ce42c5b6544c8f5967d91140c0ae06ae22" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" flutter_image_compress: @@ -539,7 +539,7 @@ packages: description: name: flutter_image_compress sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" flutter_image_compress_common: @@ -547,7 +547,7 @@ packages: description: name: flutter_image_compress_common sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.6" flutter_image_compress_macos: @@ -555,7 +555,7 @@ packages: description: name: flutter_image_compress_macos sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" flutter_image_compress_ohos: @@ -563,7 +563,7 @@ packages: description: name: flutter_image_compress_ohos sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.3" flutter_image_compress_platform_interface: @@ -571,7 +571,7 @@ packages: description: name: flutter_image_compress_platform_interface sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" flutter_image_compress_web: @@ -579,7 +579,7 @@ packages: description: name: flutter_image_compress_web sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.5" flutter_launcher_icons: @@ -587,7 +587,7 @@ packages: description: name: flutter_launcher_icons sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.14.4" flutter_lints: @@ -595,7 +595,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" flutter_localizations: @@ -608,7 +608,7 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.34" flutter_screenutil: @@ -616,7 +616,7 @@ packages: description: name: flutter_screenutil sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.9.3" flutter_smart_dialog: @@ -624,7 +624,7 @@ packages: description: name: flutter_smart_dialog sha256: "72762b20c25f8e12d490332004c9cca327f39dfc1fcba66124c6b7c108b68850" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.9.8+10" flutter_staggered_grid_view: @@ -632,7 +632,7 @@ packages: description: name: flutter_staggered_grid_view sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_svga: @@ -640,7 +640,7 @@ packages: description: name: flutter_svga sha256: c914ba2aee5e2d53775b64c7ff6530b4cdc3c27fd9348debb0d86fd68b869c39 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.13" flutter_test: @@ -658,7 +658,7 @@ packages: description: name: fluttertoast sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.2.14" glob: @@ -666,7 +666,7 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" google_identity_services_web: @@ -674,7 +674,7 @@ packages: description: name: google_identity_services_web sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+1" google_sign_in: @@ -682,7 +682,7 @@ packages: description: name: google_sign_in sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.0" google_sign_in_android: @@ -690,7 +690,7 @@ packages: description: name: google_sign_in_android sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.2.1" google_sign_in_ios: @@ -698,7 +698,7 @@ packages: description: name: google_sign_in_ios sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.9.0" google_sign_in_platform_interface: @@ -706,7 +706,7 @@ packages: description: name: google_sign_in_platform_interface sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.0" google_sign_in_web: @@ -714,7 +714,7 @@ packages: description: name: google_sign_in_web sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.4+4" gtk: @@ -722,23 +722,23 @@ packages: description: name: gtk sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" hooks: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 - url: "https://pub.dev" + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.2" + version: "1.0.3" html: dependency: transitive description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.6" http: @@ -746,7 +746,7 @@ packages: description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" http_client_helper: @@ -754,7 +754,7 @@ packages: description: name: http_client_helper sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" http_parser: @@ -762,7 +762,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" image: @@ -770,7 +770,7 @@ packages: description: name: image sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.8.0" image_cropper: @@ -785,7 +785,7 @@ packages: description: name: image_cropper_for_web sha256: "865d798b5c9d826f1185b32e5d0018c4183ddb77b7b82a931e1a06aa3b74974e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" image_cropper_platform_interface: @@ -793,7 +793,7 @@ packages: description: name: image_cropper_platform_interface sha256: ee160d686422272aa306125f3b6fb1c1894d9b87a5e20ed33fa008e7285da11e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" image_picker: @@ -801,7 +801,7 @@ packages: description: name: image_picker sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" image_picker_android: @@ -809,7 +809,7 @@ packages: description: name: image_picker_android sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.13+16" image_picker_for_web: @@ -817,7 +817,7 @@ packages: description: name: image_picker_for_web sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" image_picker_ios: @@ -825,7 +825,7 @@ packages: description: name: image_picker_ios sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.13+6" image_picker_linux: @@ -833,7 +833,7 @@ packages: description: name: image_picker_linux sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" image_picker_macos: @@ -841,7 +841,7 @@ packages: description: name: image_picker_macos sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2+1" image_picker_platform_interface: @@ -849,7 +849,7 @@ packages: description: name: image_picker_platform_interface sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.1" image_picker_windows: @@ -857,7 +857,7 @@ packages: description: name: image_picker_windows sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" in_app_purchase: @@ -865,7 +865,7 @@ packages: description: name: in_app_purchase sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.3" in_app_purchase_android: @@ -873,7 +873,7 @@ packages: description: name: in_app_purchase_android sha256: "634bee4734b17fe55f370f0ac07a22431a9666e0f3a870c6d20350856e8bbf71" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0+10" in_app_purchase_platform_interface: @@ -881,7 +881,7 @@ packages: description: name: in_app_purchase_platform_interface sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" in_app_purchase_storekit: @@ -889,7 +889,7 @@ packages: description: name: in_app_purchase_storekit sha256: "1d512809edd9f12ff88fce4596a13a18134e2499013f4d6a8894b04699363c93" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.8+1" intl: @@ -897,23 +897,23 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" iris_method_channel: dependency: transitive description: name: iris_method_channel - sha256: bfb5cfc6c6eae42da8cd1b35977a72d8b8881848a5dfc3d672e4760a907d11a0 - url: "https://pub.dev" + sha256: "114bbe541369add8dd0727858e7df5764f375e3fb88374ad487301733fddb57f" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.4" + version: "2.2.5" isolate_easy_pool: dependency: "direct main" description: name: isolate_easy_pool sha256: f0204cfdecbb84d61c46240a603bb21c3b2ac925475faf3f4afe18526fcb8f64 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.8" jni: @@ -921,7 +921,7 @@ packages: description: name: jni sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" jni_flutter: @@ -929,7 +929,7 @@ packages: description: name: jni_flutter sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" js: @@ -937,7 +937,7 @@ packages: description: name: js sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.7" json_annotation: @@ -945,7 +945,7 @@ packages: description: name: json_annotation sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.11.0" leak_tracker: @@ -953,7 +953,7 @@ packages: description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -961,7 +961,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.10" leak_tracker_testing: @@ -969,7 +969,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" lints: @@ -977,7 +977,7 @@ packages: description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" logging: @@ -985,7 +985,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" marquee: @@ -993,7 +993,7 @@ packages: description: name: marquee sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" matcher: @@ -1001,7 +1001,7 @@ packages: description: name: matcher sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.19" material_color_utilities: @@ -1009,7 +1009,7 @@ packages: description: name: material_color_utilities sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.0" meta: @@ -1017,7 +1017,7 @@ packages: description: name: meta sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.17.0" mime: @@ -1025,7 +1025,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" mime_type: @@ -1033,7 +1033,7 @@ packages: description: name: mime_type sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" native_toolchain_c: @@ -1041,7 +1041,7 @@ packages: description: name: native_toolchain_c sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.6" nested: @@ -1049,7 +1049,7 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" objective_c: @@ -1057,7 +1057,7 @@ packages: description: name: objective_c sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.3.0" octo_image: @@ -1065,7 +1065,7 @@ packages: description: name: octo_image sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" package_config: @@ -1073,7 +1073,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" package_info_plus: @@ -1081,7 +1081,7 @@ packages: description: name: package_info_plus sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.3.1" package_info_plus_platform_interface: @@ -1089,7 +1089,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" path: @@ -1097,7 +1097,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" path_drawing: @@ -1105,7 +1105,7 @@ packages: description: name: path_drawing sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" path_parsing: @@ -1113,7 +1113,7 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" path_provider: @@ -1121,7 +1121,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.5" path_provider_android: @@ -1129,7 +1129,7 @@ packages: description: name: path_provider_android sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" path_provider_foundation: @@ -1137,7 +1137,7 @@ packages: description: name: path_provider_foundation sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.0" path_provider_linux: @@ -1145,7 +1145,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -1153,7 +1153,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" path_provider_windows: @@ -1161,7 +1161,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" permission_handler: @@ -1169,7 +1169,7 @@ packages: description: name: permission_handler sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "12.0.1" permission_handler_android: @@ -1177,7 +1177,7 @@ packages: description: name: permission_handler_android sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "13.0.1" permission_handler_apple: @@ -1185,7 +1185,7 @@ packages: description: name: permission_handler_apple sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.4.7" permission_handler_html: @@ -1193,7 +1193,7 @@ packages: description: name: permission_handler_html sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.3+5" permission_handler_platform_interface: @@ -1201,7 +1201,7 @@ packages: description: name: permission_handler_platform_interface sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.0" permission_handler_windows: @@ -1209,7 +1209,7 @@ packages: description: name: permission_handler_windows sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.1" petitparser: @@ -1217,7 +1217,7 @@ packages: description: name: petitparser sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.2" photo_view: @@ -1225,7 +1225,7 @@ packages: description: name: photo_view sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.0" platform: @@ -1233,7 +1233,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.6" plugin_platform_interface: @@ -1241,7 +1241,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" posix: @@ -1249,7 +1249,7 @@ packages: description: name: posix sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.5.0" pretty_dio_logger: @@ -1257,7 +1257,7 @@ packages: description: name: pretty_dio_logger sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" protobuf: @@ -1265,7 +1265,7 @@ packages: description: name: protobuf sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.0" provider: @@ -1273,7 +1273,7 @@ packages: description: name: provider sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5+1" pub_semver: @@ -1281,7 +1281,7 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" pull_to_refresh: @@ -1289,7 +1289,7 @@ packages: description: name: pull_to_refresh sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" readmore: @@ -1297,15 +1297,23 @@ packages: description: name: readmore sha256: e8fca2bd397b86342483b409e2ec26f06560a5963aceaa39b27f30722b506187 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.0" rxdart: dependency: transitive description: name: rxdart sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.28.0" shared_preferences: @@ -1313,7 +1321,7 @@ packages: description: name: shared_preferences sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.5" shared_preferences_android: @@ -1321,7 +1329,7 @@ packages: description: name: shared_preferences_android sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.23" shared_preferences_foundation: @@ -1329,7 +1337,7 @@ packages: description: name: shared_preferences_foundation sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.6" shared_preferences_linux: @@ -1337,7 +1345,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -1345,7 +1353,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" shared_preferences_web: @@ -1353,7 +1361,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.3" shared_preferences_windows: @@ -1361,7 +1369,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" sign_in_with_apple: @@ -1369,7 +1377,7 @@ packages: description: name: sign_in_with_apple sha256: "8bd875c8e8748272749eb6d25b896f768e7e9d60988446d543fe85a37a2392b8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" sign_in_with_apple_platform_interface: @@ -1377,7 +1385,7 @@ packages: description: name: sign_in_with_apple_platform_interface sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" sign_in_with_apple_web: @@ -1385,7 +1393,7 @@ packages: description: name: sign_in_with_apple_web sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" sky_engine: @@ -1398,7 +1406,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.2" sqflite: @@ -1406,7 +1414,7 @@ packages: description: name: sqflite sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" sqflite_android: @@ -1414,7 +1422,7 @@ packages: description: name: sqflite_android sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2+3" sqflite_common: @@ -1422,7 +1430,7 @@ packages: description: name: sqflite_common sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.6" sqflite_darwin: @@ -1430,7 +1438,7 @@ packages: description: name: sqflite_darwin sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" sqflite_platform_interface: @@ -1438,7 +1446,7 @@ packages: description: name: sqflite_platform_interface sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" stack_trace: @@ -1446,7 +1454,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" stream_channel: @@ -1454,7 +1462,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" string_scanner: @@ -1462,17 +1470,17 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" + sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.4.0" + version: "3.4.0+1" tancent_vap: dependency: "direct main" description: @@ -1485,7 +1493,7 @@ packages: description: name: tencent_cloud_chat_sdk sha256: fb930c86017fa4a546f3d0c15bc5a54197f07f003934737e80cf2f9a70cab1ba - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.3.6498+3" term_glyph: @@ -1493,7 +1501,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test_api: @@ -1501,7 +1509,7 @@ packages: description: name: test_api sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.10" typed_data: @@ -1509,7 +1517,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" universal_io: @@ -1517,7 +1525,7 @@ packages: description: name: universal_io sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" url_launcher: @@ -1525,7 +1533,7 @@ packages: description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.2" url_launcher_android: @@ -1533,7 +1541,7 @@ packages: description: name: url_launcher_android sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.29" url_launcher_ios: @@ -1541,7 +1549,7 @@ packages: description: name: url_launcher_ios sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.1" url_launcher_linux: @@ -1549,7 +1557,7 @@ packages: description: name: url_launcher_linux sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" url_launcher_macos: @@ -1557,7 +1565,7 @@ packages: description: name: url_launcher_macos sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.5" url_launcher_platform_interface: @@ -1565,7 +1573,7 @@ packages: description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" url_launcher_web: @@ -1573,7 +1581,7 @@ packages: description: name: url_launcher_web sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" url_launcher_windows: @@ -1581,7 +1589,7 @@ packages: description: name: url_launcher_windows sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.5" uuid: @@ -1589,7 +1597,7 @@ packages: description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.3" vector_math: @@ -1597,7 +1605,7 @@ packages: description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" video_player: @@ -1605,7 +1613,7 @@ packages: description: name: video_player sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.1" video_player_android: @@ -1613,7 +1621,7 @@ packages: description: name: video_player_android sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.9.5" video_player_avfoundation: @@ -1621,7 +1629,7 @@ packages: description: name: video_player_avfoundation sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.9.4" video_player_platform_interface: @@ -1629,7 +1637,7 @@ packages: description: name: video_player_platform_interface sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.6.0" video_player_web: @@ -1637,7 +1645,7 @@ packages: description: name: video_player_web sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" video_thumbnail: @@ -1645,7 +1653,7 @@ packages: description: name: video_thumbnail sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" visibility_detector: @@ -1653,39 +1661,39 @@ packages: description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0+2" vm_service: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" + sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" + url: "https://pub.flutter-io.cn" source: hosted - version: "15.0.2" + version: "15.1.0" wakelock_plus: dependency: "direct main" description: name: wakelock_plus sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "24b84143787220a403491c2e5de0877fbbb87baf3f0b18a2a988973863db4b03" - url: "https://pub.dev" + sha256: "14b2e5b9e35c2631e656913c47adecdd71633ae92896a27a64c8f1fcfabc21cc" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.0" + version: "1.5.0" web: dependency: transitive description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" webview_flutter: @@ -1693,7 +1701,7 @@ packages: description: name: webview_flutter sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.4.2" webview_flutter_android: @@ -1701,7 +1709,7 @@ packages: description: name: webview_flutter_android sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.16.9" webview_flutter_platform_interface: @@ -1709,23 +1717,23 @@ packages: description: name: webview_flutter_platform_interface sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.15.1" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: d7219cfabc6f5fc2032e0fa980ec36d71f308a35a823395af1abc34d9a2ede83 - url: "https://pub.dev" + sha256: e15d8828e014291324a4d0cf6e272090167f4fa5673ffcf8fe446f4a4cd35861 + url: "https://pub.flutter-io.cn" source: hosted - version: "3.24.2" + version: "3.24.3" win32: dependency: transitive description: name: win32 sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.15.0" win32_registry: @@ -1733,7 +1741,7 @@ packages: description: name: win32_registry sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.5" xdg_directories: @@ -1741,7 +1749,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" xml: @@ -1749,7 +1757,7 @@ packages: description: name: xml sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.6.1" yaml: @@ -1757,9 +1765,9 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" diff --git a/pubspec.yaml b/pubspec.yaml index 797ca6f..a5ecc80 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 1.2.0+4 environment: diff --git a/sc_images/general/room_id.svga b/sc_images/general/room_id.svga new file mode 100644 index 0000000..4e6234d Binary files /dev/null and b/sc_images/general/room_id.svga differ diff --git a/sc_images/general/room_id_custom.svga b/sc_images/general/room_id_custom.svga new file mode 100644 index 0000000..0e783cb Binary files /dev/null and b/sc_images/general/room_id_custom.svga differ diff --git a/sc_images/general/user_id.svga b/sc_images/general/user_id.svga new file mode 100644 index 0000000..739c689 Binary files /dev/null and b/sc_images/general/user_id.svga differ diff --git a/sc_images/general/user_id_4.svga b/sc_images/general/user_id_4.svga new file mode 100644 index 0000000..76c66a7 Binary files /dev/null and b/sc_images/general/user_id_4.svga differ diff --git a/sc_images/room/anim/gift/room_gift_combo_badge.png b/sc_images/room/anim/gift/room_gift_combo_badge.png index 3555a39..bd3ec8f 100644 Binary files a/sc_images/room/anim/gift/room_gift_combo_badge.png and b/sc_images/room/anim/gift/room_gift_combo_badge.png differ diff --git a/需求进度.md b/需求进度.md index feedc42..e61fb45 100644 --- a/需求进度.md +++ b/需求进度.md @@ -3,6 +3,27 @@ ## 当前总目标 - 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。 +## 本轮靓号动效替换(已完成) +- 已按 2026-04-21 最新靓号动效需求,把桌面“靓号动效”里的 `room_id / room_id_custom / user_id / user_id_4` 四套 `SVGA` 素材导入 Flutter 工程,并新增统一的靓号 ID 展示组件;当前会按 `ownSpecialId != null && expiredTime > now` 判定是否仍为有效靓号,过期靓号会自动回退到原始账号展示。 +- 已完成 4 个指定场景的替换:`room_id` 现用于房间详情里的房间号,`room_id_custom` 现用于房间详情里的房主 ID,`user_id` 现用于房间在线列表和房间个人卡片,`user_id_4` 现用于个人主页和 `Me` 页;对应文案在命中靓号时会展示靓号号码,未命中时保持原始 `ID:` 文本样式。 + +## 本轮房间动效优化(进行中) +- 已按 2026-04-21 当前房间卡顿优化方案先落第一步“统一调度层”:新增房间特效调度器,在全屏 `VAP/SVGA` 高成本特效播放或排队期间,暂缓低优先级的房间进场动画、全局飘屏和座位飞屏入队,待高成本特效清空后按小间隔续播,先减少多条动画链路同一时刻抢主线程的情况。 +- 本步只处理“调度时机”,没有改动现有动画内部逻辑:礼物/飞屏/飘屏的现有倍率触发规则、现有上限截断、`customAnimationCount` 现有循环方式和当前视觉表现均保持原样,避免第一步就引入联动回归。 +- 已继续落第二步“局部重建/重绘隔离”:房间礼物播报条已从整层 `Consumer` 重建改成每个动画槽位独立 `Selector` 订阅,并把运行态数据拍平成快照,减少 `notifyListeners()` 时 4 个槽位整层一起重建;同时为礼物播报条、进场动画、座位飞屏和全局飘屏 overlay 补充 `RepaintBoundary`,先把高频位移动画和底层页面绘制隔离开,降低同屏重绘面积。 +- 第二步同样没有改动任何动画业务规则:礼物播报条合并策略、幸运礼物奖励展示、现有队列时长、`customAnimationCount` 的逐个入队方式、现有上限和触发条件都保持原样,这一步只收敛 rebuild / repaint 成本。 +- 已顺手补上高成本特效资源预热接线:当前全屏 `VAP/SVGA` 任务一入队就会触发已有的 preload 逻辑,让排队中的特效在前一个播放期间优先完成 `svga decode / 网络文件落缓存`,减少切到下一段时的首帧等待;这一步复用现有预热能力,不改变任何播放顺序和触发条件。 +- 已继续补上图片链路的“按展示尺寸解码 + 排队阶段预热”:`buildCachedImageProvider` 现已支持 `network/file/asset` 三类资源统一按逻辑尺寸生成 provider,并通过 `ResizeImage` 把解码尺寸压到实际展示大小;同时房间礼物播报条、进场动画、全局飘屏与座位飞屏在入队时会对头像/礼物图做异步预热,减少正式播到该条动画时首次解码和图片闪一下的概率。 +- 这一轮同样没有改动任何动画规则或队列行为:只补资源命中率和解码尺寸控制,不变更现有播放次数、上限、倍率阈值、`customAnimationCount` 逐个飞行方式和当前视觉逻辑。 +- 已继续收口座位飞屏的图片命中链路:当前在真正批量入队逐个飞行前,会先按飞行动画的大图尺寸预热一次礼物图;同时飞屏 overlay 内部会复用同路径同尺寸的 `ImageProvider`,减少 burst 场景里同一张礼物图连续多次构造 provider 和重复命中解析链路的开销。 +- 已继续专项收口“顶部礼物信息播报”的连击卡顿:当前已把礼物播报条的“整条入场动画”和“右侧 `xN` 数量脉冲/续命”拆成独立状态,连击到来时不再通过重启整条 5 秒控制器来硬续播,而是只刷新数量脉冲并延长当前播报条的空闲存活时间;同时右侧数量已改为从当前显示值平滑补间到目标值,减少高频连击或本地批量合并时出现的数字跳变、断档和忽大忽小。 +- 这一轮同样没有改礼物业务判断口径:未调整礼物连击的触发条件、上限、截断、倍率门槛和 `customAnimationCount` 等既有规则,只优化顶部播报条自身的显示状态机与数量刷新方式。 +- 已继续修正连击播报条“单点送 1 个却直接跳 `+5`”的问题:顶部礼物播报条现已接入 `customAnimationCount` 的点击粒度信息,数量动画不再只按合并后的总量做补间,而是按“单次点击对应的数量步长”逐步递进;例如连续点击 5 次、每次送 1 个时,会按 `+1 +1 +1 +1 +1` 的视觉节奏补齐,而不是直接把右侧数量跳成 `+5`。 +- 已继续修正连击档位特效偶发“不播”的问题:当前连击 `SVGA` 触发已改为只命中仓库里真实存在资源的档位集合,不再把 `20/88/200...` 这类当前工程里没有对应特效文件的档位当成可播放资源;同时命中判断会按“本次新增后跨过的最高有效资源档位”来触发,避免因为本地批量窗口把多次点击合成一包后,先命中到空资源路径而看起来像整段特效都没播。 +- 已按 2026-04-21 最新回归继续修正“飞向麦位的自制动画”缺失问题:座位飞屏现已从房间统一调度器的延后队列中移出,恢复为收到礼物消息后直接入队到全局 `RoomGiftSeatFlightOverlay`;保留既有的图片预热、同会话队列上限和 `customAnimationCount` 逐个飞行逻辑,只撤回此前对这一核心反馈动画的延迟调度,避免用户在礼物命中全屏特效或调度状态未及时清空时误以为飞屏动画消失。 +- 已继续加固座位飞屏退出房间后的残留风险:全局 `RoomGiftSeatFlightOverlay` 现已和房间可视特效开关绑定,离开房间后不会再继续挂在首页/探索/消息/我的等页面上显示;同时 overlay 内部的 `postFrame / delayed retry / image precache` 回调已补上会话 token 校验,避免房间退出后旧动画任务把屏幕中央的静态礼物图再次拉起并长期悬挂。 +- 已继续修正“房间飘屏在首页/探索/消息/我的也会刷到”的问题:根层房间礼物特效层现已统一改为依据 `RtcProvider.shouldShowRoomVisualEffects` 判断显示,而不再只看偏松的 `roomVisualEffectsEnabled`;同时 `OverlayManager` 在播放房间礼物/幸运礼物飘屏前会再次校验“当前仍在房间可视态且消息房间号与当前房间一致”,房间最小化或退出时也会主动清掉正在播和排队中的房间飘屏,避免房间内的 `5 times`、礼物飘屏等效果溢出到首页等非房间页面。 + ## 本轮启动优化(非网络) - 已补回启动页正式展示逻辑:当前 `Weekly Star / Last Weekly CP` 两套自定义启动页都改为“本地有缓存才展示、无缓存不展示”;由于现阶段周榜与 CP 榜接口链路都还未 ready,缓存刷新逻辑已先关闭,所以当前启动阶段会直接回退到默认 splash,不再展示这两套定制视觉稿。相关恢复入口已在缓存类里用 `api-ready-launch-splash` 注释标记,后续接口 ready 后可直接搜索接回。 - 已将启动页展示时间收敛为 `3` 秒,并在右上角新增通用 `skip 倒计时` 按钮:当前按钮会按秒级动态展示剩余时间,点击可立即跳过;文案已补齐 `en/ar/tr/bn` 多语言翻译,并按 locale 输出倒计时文本,便于后续继续做 RTL 语言验收。 @@ -41,6 +62,18 @@ - 已继续修正幸运礼物 `burst` 中央金额文案的细节对位:当前已再上移 `2` 个单位,并给金额文本左侧补出额外留白,避免斜体 `+` 号因为字形外扩而贴边或被裁掉,确保 `+金币` 能完整落在特效中部。 - 已继续按最新联调口径修正幸运礼物 `burst` 中央金额文案:当前已去掉文本里的 `+` 字符,仅保留金币数本身显示;同时维持上一版向上微调后的纵向位置,让金额落点保持在特效中部偏上的稳定区域。 - 已继续收口幸运礼物 `burst` 金额文案与 `svga` 本体的时机同步:此前中央金币文本直接跟外层中奖数据显隐,而 `svga` 自身还存在资源加载和单次播放完成的生命周期,所以两者在出现/消失时会有肉眼可见的前后差;当前已给 `SCSvgaAssetWidget` 补上播放开始/结束回调,并让 `burst` 中央金额只在 `svga` 真正开始播时显示、在它播完清帧时一并隐藏。 +- 已按 2026-04-21 最新联调继续细调幸运礼物 `burst` 中央金币文案:当前已在现有基础上再向上微调 `4` 个单位、向右微调 `5` 个单位,让金额更贴近特效中部的目标槽位;同时 `burst` 的整体展示时长也已再缩短 `1` 秒,避免命中后在屏上停留过久。 +- 已继续修复幸运礼物顶部横幅 `from` 后礼物图标偶发显示不出来的问题:此前这条紫色 lucky gift 横幅只直接使用 socket 下发的 `giftCover/giftUrl` 渲染礼物图,一旦服务端该字段为空或图片加载失败,就只会退回默认占位;当前已把 `giftId` 一并挂进 `SCFloatingMessage`,并在 `FloatingLuckGiftScreenWidget` 内增加“优先用 `giftUrl`,失败时再回退到本地礼物列表缓存中的 `giftPhoto`”的双重兜底,避免横幅末尾再出现空白占位块。 +- 已继续补齐幸运礼物横幅礼物图标的“晚到数据”刷新:此前 `from` 后面的礼物图只在横幅首次 build 时用 `listen: false` 读取一次本地礼物列表,若 `giftList` 之后才加载完成,这条 overlay 生命周期内也不会再更新,最终仍会留下空白图标;当前已改为通过 `Selector` 监听 `giftId -> giftPhoto` 回退图,并在 socket 未携带 `giftUrl` 时主动补拉一次 `giftList`,让礼物列表就绪后能把幸运礼物横幅末尾图标自动补出来。 +- 已继续修正幸运礼物紫色横幅里 `from` 后礼物图“闪一下又空白”的问题:排查后发现这条横幅把礼物图标塞在 `Text.rich` 末尾,而正文又开启了单行省略,导致尾部 `WidgetSpan` 在最终排版溢出时可能被直接不绘制;当前已把礼物图标从内联文本里拆到正文右侧单独布局,保持正文自己省略、礼物图标固定显示,不再因为文本溢出把尾部礼物图一起吃掉。 +- 已按 2026-04-21 最新回归把紫色幸运礼物横幅的礼物图取值收口到和其它飘屏同一来源:当前 `RTM` 在构造幸运礼物 `SCFloatingMessage` 前,会优先按 `giftId/standardId` 从本地礼物列表拿同款 `giftPhoto`,拿不到时才退回服务端下发的 `giftCover`;同时紫色横幅组件本身不再额外做自己的礼物图回退和二次解析,只直接显示已统一好的 `message.giftUrl`,避免和其它飘屏走出两套不同的取值链路。 +- 已按 2026-04-21 最新排障请求给紫色幸运礼物横幅补齐定向日志:当前会在 `RTM` 解析 `GAME_LUCKY_GIFT` 时输出 `giftId / giftCover / 本地 id 命中 / 本地 standardId 命中 / resolvedUrl`,同时横幅组件自身也会记录一次 `init`、`gift icon render`、`gift icon url empty` 和 `gift icon load failed`,方便直接判断是服务端字段为空、本地映射错位,还是图片请求本身失败。 +- 已根据 2026-04-21 最新日志结论继续收口幸运礼物紫色横幅:日志已确认这条横幅实际拿到的 `giftUrl` 正常且组件已进入渲染分支,因此当前继续把礼物图标的绘制方式直接对齐到其它飘屏的朴素路径,改为更大的 `netImage(url, width)` 直出并使用 `BoxFit.contain`,不再叠加此前那套更特殊的小图标 `borderRadius/noDefaultImg` 参数,优先排除该组件私有渲染参数导致的异常显示。 +- 已继续绕开幸运礼物紫色横幅里可疑的小图 `ExtendedImage` 渲染链:当前礼物图标已改为在 `initState` 里先构建 `buildCachedImageProvider`,再通过基础 `Image(image: provider)` 直接绘制,并额外输出一次 `gift icon frame ready` 日志,用于确认这张图是否真的完成首帧解码显示;这样可以把问题进一步收敛为“图片 provider/解码”还是“组件布局/视觉观感”。 +- 已按 2026-04-21 最新联调请求,把紫色幸运礼物横幅 `from` 后的图标临时替换为金币图标:这一步只用于快速验证该图标位本身的布局和可见性是否正常,不再受当前礼物图资源链路影响;如果金币图标能稳定显示,就说明问题仍集中在礼物图渲染链,而不是这个位置被遮挡或根本没画出来。 +- 已按 2026-04-21 最新回归把 [floating_luck_gift_screen_widget] 的样式撤回到最初实现,不再继续在幸运礼物飘屏横幅上做偏题调试;同时已定位到真正缺图的是消息栏里的 `gameLuckyGift_5` 高亮消息,这条消息此前只写入了 `awardAmount/user/msg`,没有把 `gift` 一并塞进去,导致 `room_msg_item.dart` 读取 `widget.msg.gift?.giftPhoto` 时天然为空。当前已在 `RTM` 构造高亮 lucky 消息时补回 `giftPhoto`,让消息栏里的紫色 lucky message 能和其它消息一样拿到礼物图。 +- 已按 2026-04-21 最新特效需求继续调整幸运礼物 `burst`:在 `luck_gift_reward_burst.svga` 下方那块淡色圆角矩形区域,补上了与 `SVGA` 同步进场/消失的中奖倍数文案;当前会和中央金额文本共用同一显隐时机,并在 `multiple > 0` 时展示为 `xN`,避免出现 `x0` 这类无效信息。 +- 已继续按 2026-04-21 最新特效稿给幸运礼物 `burst` 补上中奖发送者头像:当前头像会出现在你标的黑块位置附近,并和 `SVGA` 使用同一套开始/结束显隐时机同步出现与消失;头像显示只读取当前中奖事件的 `userAvatar`,不改现有 `burst` 触发条件和播放时长。 - 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。 - 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。 - 已继续微调语言房个人卡片与送礼 UI:个人卡片底部动作文案现已支持两行居中展示,避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。 @@ -179,11 +212,18 @@ - `lib/shared/data_sources/models/enum/sc_gift_type.dart` - `lib/shared/tools/sc_network_image_utils.dart` - `lib/shared/tools/sc_gift_vap_svga_manager.dart` +- `lib/shared/tools/sc_room_effect_scheduler.dart` - `lib/services/general/sc_app_general_manager.dart` +- `lib/shared/data_sources/sources/local/floating_screen_manager.dart` - `lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart` - `lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart` - `lib/services/room/rc_room_manager.dart` +- `lib/services/gift/gift_animation_manager.dart` - `lib/services/audio/rtc_manager.dart` +- `lib/modules/room/voice_room_page.dart` +- `lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart` +- `lib/ui_kit/widgets/room/anim/room_entrance_screen.dart` +- `lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart` - `lib/modules/room/edit/room_edit_page.dart` - `lib/modules/user/edit/edit_user_info_page2.dart` - `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart` @@ -205,6 +245,12 @@ - `lib/ui_kit/widgets/room/room_game_bottom_sheet.dart` - `lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart` - `lib/modules/room/detail/room_detail_page.dart` +- `lib/modules/room/online/room_online_page.dart` +- `lib/modules/user/me_page2.dart` +- `lib/modules/user/profile/person_detail_page.dart` +- `lib/shared/business_logic/models/res/login_res.dart` +- `lib/ui_kit/widgets/id/sc_special_id_badge.dart` +- `lib/ui_kit/widgets/room/room_user_info_card.dart` - `lib/modules/home/popular/party/sc_home_party_page.dart` - `lib/modules/home/popular/mine/sc_home_mine_skeleton.dart` - `lib/modules/home/popular/follow/sc_room_follow_page.dart` @@ -225,6 +271,10 @@ - `lib/modules/user/my_items/theme/bags_theme_page.dart` - `lib/ui_kit/widgets/store/store_bag_page_helpers.dart` - `sc_images/general/sc_no_data.png` +- `sc_images/general/room_id.svga` +- `sc_images/general/room_id_custom.svga` +- `sc_images/general/user_id.svga` +- `sc_images/general/user_id_4.svga` - `sc_images/splash/sc_weekly_star_bg.png` - `sc_images/splash/sc_icon_weekly_star_rank_1.png` - `sc_images/splash/sc_icon_weekly_star_rank_2.png` @@ -385,6 +435,13 @@ - Launch image 仍是默认占位资源,提交前建议替换 - `android/key.properties` 保存了本地 upload keystore 口令,必须自行妥善备份,且不要提交到仓库 - 由于已显式移除 Agora 本地屏幕共享相关组件,如果业务后续要启用“屏幕共享/录屏推流”,需要再单独恢复相关 manifest 声明 +- `burst` 特效叠加层已停止用 `Align + translate` 猜偏移,改为按 `luck_gift_reward_burst.svga` 拆出的参考图层比例重排头像、中奖金额、倍数,重点对齐头像黑色占位圈和底部倍数圆角块 +- 根据最新真机截图,`burst` 叠加层已整体继续向右下微调,先修正“整体偏左上”的问题,不改头像和文案尺寸 +- 按最新口头要求,`burst` 叠加层又整体额外向下平移 `40` 个设计单位,继续保持头像、金额、倍数三者相对位置不变 +- `burst` 头像已按最新要求缩到原来的 `80%`,并同步补偿左上坐标,保持头像中心点仍然对准原坑位 +- `burst` 头像继续按最新口头要求单独微调:向下 `10` 个设计单位、向左 `2` 个设计单位,其余金额和倍数位置不动 +- `burst` 头像又继续单独向下补移 `8` 个设计单位,当前累计为向下 `18`、向左 `2` +- 飞向麦位动画补了统一的“中心图空闲同步”逻辑:目标坐标重试失败放弃、队列被清空、上一段飞行结束后,都会把中心静态礼物图切到队列头或直接清空,避免残留卡死在屏幕中央 ## 下一步要做什么 - 将新的 `AAB` 上传到 Play Console,并在首发流程中继续使用 Google 管理的 app signing key。