bug fix
This commit is contained in:
parent
7e136f4d7d
commit
c4e177dd7e
335
docs/voice-room-emoji-plan.md
Normal file
335
docs/voice-room-emoji-plan.md
Normal file
@ -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
|
||||
@ -502,7 +502,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -511,7 +511,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.1;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -693,7 +693,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -702,7 +702,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.1;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -722,7 +722,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -731,7 +731,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.1;
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@ -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<RoomDetailPage> {
|
||||
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<RoomDetailPage> {
|
||||
),
|
||||
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<RoomDetailPage> {
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.userProfile
|
||||
?.account ??
|
||||
?.getID() ??
|
||||
"",
|
||||
),
|
||||
);
|
||||
|
||||
@ -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: () {
|
||||
|
||||
@ -28,6 +28,7 @@ class SCSeatItem extends StatefulWidget {
|
||||
class _SCSeatItemState extends State<SCSeatItem> 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<SCSeatItem> 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<SCSeatItem> 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<SCSeatItem> 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(
|
||||
|
||||
@ -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<MePage2> {
|
||||
),
|
||||
),
|
||||
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,
|
||||
|
||||
@ -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<PersonDetailPage>
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -568,7 +568,7 @@ class SocialChatUserProfile {
|
||||
///获取Id,有靓号显示靓号
|
||||
String getID() {
|
||||
String uid = account ?? "";
|
||||
if (_ownSpecialId != null) {
|
||||
if (hasSpecialId()) {
|
||||
uid = _ownSpecialId?.account ?? "";
|
||||
}
|
||||
return uid;
|
||||
|
||||
145
lib/ui_kit/widgets/id/sc_special_id_badge.dart
Normal file
145
lib/ui_kit/widgets/id/sc_special_id_badge.dart
Normal file
@ -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 = <Widget>[
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -256,6 +256,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
_ensureCenterVisual(request);
|
||||
_trimQueuedRequestsOverflow();
|
||||
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
||||
_syncIdleCenterVisual();
|
||||
_scheduleNextAnimation();
|
||||
}
|
||||
|
||||
@ -328,17 +329,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
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) {
|
||||
@ -406,6 +397,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
_scheduleNextAnimation();
|
||||
});
|
||||
} else {
|
||||
_syncIdleCenterVisual();
|
||||
_scheduleNextAnimation();
|
||||
}
|
||||
return;
|
||||
@ -443,28 +435,21 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -472,7 +457,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
if (_centerRequest != null && _centerImageProvider != null) {
|
||||
return;
|
||||
}
|
||||
final imageProvider = _resolveImageProvider(request.imagePath);
|
||||
final imageProvider = _resolveImageProvider(
|
||||
request.imagePath,
|
||||
request: request,
|
||||
);
|
||||
if (!mounted) {
|
||||
_centerRequest = request;
|
||||
_centerImageProvider = imageProvider;
|
||||
@ -484,6 +472,39 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
@ -507,8 +528,11 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
return overlayBox.globalToLocal(globalTargetCenter);
|
||||
}
|
||||
|
||||
ImageProvider<Object> _resolveImageProvider(String imagePath) {
|
||||
final currentRequest = _activeRequest ?? _centerRequest;
|
||||
ImageProvider<Object> _resolveImageProvider(
|
||||
String imagePath, {
|
||||
RoomGiftSeatFlightRequest? request,
|
||||
}) {
|
||||
final currentRequest = request ?? _activeRequest ?? _centerRequest;
|
||||
final logicalSize =
|
||||
currentRequest == null
|
||||
? null
|
||||
|
||||
@ -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<LuckGiftNomorAnimWidget> {
|
||||
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<LuckGiftNomorAnimWidget> {
|
||||
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<LuckGiftNomorAnimWidget> {
|
||||
_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<String>(
|
||||
'${rewardData.sendUserId ?? ""}'
|
||||
'|${rewardData.acceptUserId ?? ""}'
|
||||
@ -93,37 +172,83 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
},
|
||||
),
|
||||
),
|
||||
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(5.w, -4.w),
|
||||
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: 0.w, right: 0.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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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<RoomUserInfoCard> {
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
470
pubspec.lock
470
pubspec.lock
File diff suppressed because it is too large
Load Diff
@ -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.1.1+3
|
||||
version: 1.2.0+4
|
||||
|
||||
|
||||
environment:
|
||||
|
||||
BIN
sc_images/general/room_id.svga
Normal file
BIN
sc_images/general/room_id.svga
Normal file
Binary file not shown.
BIN
sc_images/general/room_id_custom.svga
Normal file
BIN
sc_images/general/room_id_custom.svga
Normal file
Binary file not shown.
BIN
sc_images/general/user_id.svga
Normal file
BIN
sc_images/general/user_id.svga
Normal file
Binary file not shown.
BIN
sc_images/general/user_id_4.svga
Normal file
BIN
sc_images/general/user_id_4.svga
Normal file
Binary file not shown.
23
需求进度.md
23
需求进度.md
@ -3,6 +3,10 @@
|
||||
## 当前总目标
|
||||
- 控制当前 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` 现有循环方式和当前视觉表现均保持原样,避免第一步就引入联动回归。
|
||||
@ -68,6 +72,8 @@
|
||||
- 已继续绕开幸运礼物紫色横幅里可疑的小图 `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`,保持房间底栏视觉和最新动效稿一致。
|
||||
@ -239,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`
|
||||
@ -259,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`
|
||||
@ -419,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。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user