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;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -511,7 +511,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.1;
|
MARKETING_VERSION = 1.2.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -693,7 +693,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -702,7 +702,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.1;
|
MARKETING_VERSION = 1.2.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -722,7 +722,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -731,7 +731,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.1;
|
MARKETING_VERSION = 1.2.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
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/sc_tts.dart';
|
||||||
import '../../../ui_kit/components/dialog/dialog_base.dart';
|
import '../../../ui_kit/components/dialog/dialog_base.dart';
|
||||||
import '../../../ui_kit/components/text/sc_text.dart';
|
import '../../../ui_kit/components/text/sc_text.dart';
|
||||||
|
import '../../../ui_kit/widgets/id/sc_special_id_badge.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../index/main_route.dart';
|
import '../../index/main_route.dart';
|
||||||
|
|
||||||
@ -153,10 +154,41 @@ class _RoomDetailPageState extends State<RoomDetailPage> {
|
|||||||
SizedBox(height: 5.w),
|
SizedBox(height: 5.w),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
text(
|
SCSpecialIdBadge(
|
||||||
"ID:${ref.currenRoom?.roomProfile?.userProfile?.getID()}",
|
idText:
|
||||||
fontSize: 12.sp,
|
ref
|
||||||
textColor: Colors.black,
|
.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),
|
SizedBox(width: 3.w),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@ -278,10 +310,44 @@ class _RoomDetailPageState extends State<RoomDetailPage> {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
text(
|
SCSpecialIdBadge(
|
||||||
"ID:${ref.currenRoom?.roomProfile?.userProfile?.account}",
|
idText:
|
||||||
textColor: Colors.black,
|
ref
|
||||||
fontSize: 14.sp,
|
.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),
|
SizedBox(width: 3.w),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@ -304,7 +370,7 @@ class _RoomDetailPageState extends State<RoomDetailPage> {
|
|||||||
.currenRoom
|
.currenRoom
|
||||||
?.roomProfile
|
?.roomProfile
|
||||||
?.userProfile
|
?.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_page_list.dart';
|
||||||
import '../../../ui_kit/components/sc_tts.dart';
|
import '../../../ui_kit/components/sc_tts.dart';
|
||||||
import '../../../ui_kit/components/text/sc_text.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 '../../../shared/tools/sc_lk_dialog_util.dart';
|
||||||
import '../../../ui_kit/widgets/room/room_user_info_card.dart';
|
import '../../../ui_kit/widgets/room/room_user_info_card.dart';
|
||||||
|
|
||||||
@ -148,22 +149,26 @@ class _RoomOnlinePageState
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(vertical: 3.w),
|
padding: EdgeInsets.symmetric(vertical: 3.w),
|
||||||
child: Row(
|
child: SCSpecialIdBadge(
|
||||||
textDirection: TextDirection.ltr,
|
idText: userInfo.getID(),
|
||||||
children: [
|
showAnimated: userInfo.hasSpecialId(),
|
||||||
text(
|
assetPath: SCSpecialIdAssets.userId,
|
||||||
"ID:${userInfo.getID()}",
|
animationWidth: 110.w,
|
||||||
fontSize: 12.sp,
|
animationHeight: 28.w,
|
||||||
textColor: Colors.black,
|
textPadding: EdgeInsets.fromLTRB(33.w, 6.w, 16.w, 6.w),
|
||||||
fontWeight: FontWeight.bold,
|
animationTextStyle: TextStyle(
|
||||||
),
|
color: Colors.white,
|
||||||
SizedBox(width: 5.w),
|
fontSize: 12.sp,
|
||||||
Image.asset(
|
fontWeight: FontWeight.bold,
|
||||||
"sc_images/room/sc_icon_user_card_copy_id.png",
|
),
|
||||||
width: 12.w,
|
normalTextStyle: TextStyle(
|
||||||
height: 12.w,
|
color: Colors.black,
|
||||||
),
|
fontSize: 12.sp,
|
||||||
],
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
showCopyIcon: true,
|
||||||
|
copyIconSize: 12.w,
|
||||||
|
copyIconSpacing: 5.w,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ class SCSeatItem extends StatefulWidget {
|
|||||||
class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||||
static const String _seatHeartbeatValueIconAsset =
|
static const String _seatHeartbeatValueIconAsset =
|
||||||
"sc_images/room/sc_icon_room_seat_heartbeat_value.png";
|
"sc_images/room/sc_icon_room_seat_heartbeat_value.png";
|
||||||
|
static const double _seatHeaddressScaleMultiplier = 1.3;
|
||||||
|
|
||||||
RtcProvider? provider;
|
RtcProvider? provider;
|
||||||
_SeatRenderSnapshot? _cachedSnapshot;
|
_SeatRenderSnapshot? _cachedSnapshot;
|
||||||
@ -56,6 +57,8 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
|||||||
window.locale.languageCode == "ar"
|
window.locale.languageCode == "ar"
|
||||||
? ""
|
? ""
|
||||||
: seatSnapshot.headdressSourceUrl;
|
: seatSnapshot.headdressSourceUrl;
|
||||||
|
final double seatBaseSize = widget.isGameModel ? 40.w : 55.w;
|
||||||
|
final bool hasHeaddress = resolvedHeaddress.isNotEmpty;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
@ -66,9 +69,10 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
key: _targetKey,
|
key: _targetKey,
|
||||||
width: widget.isGameModel ? 40.w : 55.w,
|
width: seatBaseSize,
|
||||||
height: widget.isGameModel ? 40.w : 55.w,
|
height: seatBaseSize,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Sonic(
|
Sonic(
|
||||||
@ -77,10 +81,15 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
|||||||
isGameModel: widget.isGameModel,
|
isGameModel: widget.isGameModel,
|
||||||
),
|
),
|
||||||
seatSnapshot.hasUser
|
seatSnapshot.hasUser
|
||||||
? head(
|
? Transform.scale(
|
||||||
url: seatSnapshot.userAvatar,
|
scale:
|
||||||
width: widget.isGameModel ? 40.w : 55.w,
|
hasHeaddress ? _seatHeaddressScaleMultiplier : 1,
|
||||||
headdress: resolvedHeaddress,
|
child: head(
|
||||||
|
url: seatSnapshot.userAvatar,
|
||||||
|
width: seatBaseSize,
|
||||||
|
height: seatBaseSize,
|
||||||
|
headdress: resolvedHeaddress,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: (seatSnapshot.micLock
|
: (seatSnapshot.micLock
|
||||||
? Image.asset(
|
? 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/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_compontent.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.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 {
|
class MePage2 extends StatefulWidget {
|
||||||
const MePage2({super.key});
|
const MePage2({super.key});
|
||||||
@ -153,9 +154,19 @@ class _MePage2State extends State<MePage2> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 2.w),
|
SizedBox(height: 2.w),
|
||||||
Text(
|
SCSpecialIdBadge(
|
||||||
'ID:${profile?.account ?? ''}',
|
idText: profile?.getID() ?? '',
|
||||||
style: TextStyle(
|
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,
|
color: Colors.white,
|
||||||
fontSize: 16.sp,
|
fontSize: 16.sp,
|
||||||
fontWeight: FontWeight.w600,
|
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_counter_res.dart';
|
||||||
import '../../../shared/business_logic/models/res/sc_user_identity_res.dart';
|
import '../../../shared/business_logic/models/res/sc_user_identity_res.dart';
|
||||||
import '../../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.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';
|
import '../../chat/chat_route.dart';
|
||||||
|
|
||||||
class PersonDetailPage extends StatefulWidget {
|
class PersonDetailPage extends StatefulWidget {
|
||||||
@ -398,11 +399,23 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(width: 25.w),
|
SizedBox(width: 25.w),
|
||||||
text(
|
SCSpecialIdBadge(
|
||||||
"ID:${ref.userProfile?.account ?? ""}",
|
idText: ref.userProfile?.getID() ?? "",
|
||||||
textColor: Colors.white,
|
showAnimated: ref.userProfile?.hasSpecialId() ?? false,
|
||||||
fontWeight: FontWeight.w400,
|
assetPath: SCSpecialIdAssets.userIdLarge,
|
||||||
fontSize: 16.sp,
|
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,有靓号显示靓号
|
///获取Id,有靓号显示靓号
|
||||||
String getID() {
|
String getID() {
|
||||||
String uid = account ?? "";
|
String uid = account ?? "";
|
||||||
if (_ownSpecialId != null) {
|
if (hasSpecialId()) {
|
||||||
uid = _ownSpecialId?.account ?? "";
|
uid = _ownSpecialId?.account ?? "";
|
||||||
}
|
}
|
||||||
return uid;
|
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);
|
_ensureCenterVisual(request);
|
||||||
_trimQueuedRequestsOverflow();
|
_trimQueuedRequestsOverflow();
|
||||||
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
||||||
|
_syncIdleCenterVisual();
|
||||||
_scheduleNextAnimation();
|
_scheduleNextAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,17 +329,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
for (final queuedRequest in requestsToRemove) {
|
for (final queuedRequest in requestsToRemove) {
|
||||||
_queue.remove(queuedRequest);
|
_queue.remove(queuedRequest);
|
||||||
}
|
}
|
||||||
if (!_isPlaying && _queue.isEmpty) {
|
_syncIdleCenterVisual();
|
||||||
if (!mounted) {
|
|
||||||
_centerRequest = null;
|
|
||||||
_centerImageProvider = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_centerRequest = null;
|
|
||||||
_centerImageProvider = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int _countTrackedRequests(String queueTag) {
|
int _countTrackedRequests(String queueTag) {
|
||||||
@ -406,6 +397,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
_scheduleNextAnimation();
|
_scheduleNextAnimation();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
_syncIdleCenterVisual();
|
||||||
_scheduleNextAnimation();
|
_scheduleNextAnimation();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -443,28 +435,21 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _finishCurrentAnimation() {
|
void _finishCurrentAnimation() {
|
||||||
final hasPendingRequests = _queue.isNotEmpty;
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
if (!hasPendingRequests) {
|
|
||||||
_centerRequest = null;
|
|
||||||
_centerImageProvider = null;
|
|
||||||
}
|
|
||||||
_activeRequest = null;
|
_activeRequest = null;
|
||||||
_activeImageProvider = null;
|
_activeImageProvider = null;
|
||||||
_activeTargetOffset = null;
|
_activeTargetOffset = null;
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
|
_syncIdleCenterVisual();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
if (!hasPendingRequests) {
|
|
||||||
_centerRequest = null;
|
|
||||||
_centerImageProvider = null;
|
|
||||||
}
|
|
||||||
_activeRequest = null;
|
_activeRequest = null;
|
||||||
_activeImageProvider = null;
|
_activeImageProvider = null;
|
||||||
_activeTargetOffset = null;
|
_activeTargetOffset = null;
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
});
|
});
|
||||||
|
_syncIdleCenterVisual();
|
||||||
_scheduleNextAnimation();
|
_scheduleNextAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -472,7 +457,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
if (_centerRequest != null && _centerImageProvider != null) {
|
if (_centerRequest != null && _centerImageProvider != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final imageProvider = _resolveImageProvider(request.imagePath);
|
final imageProvider = _resolveImageProvider(
|
||||||
|
request.imagePath,
|
||||||
|
request: request,
|
||||||
|
);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
_centerRequest = request;
|
_centerRequest = request;
|
||||||
_centerImageProvider = imageProvider;
|
_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) {
|
Offset? _resolveTargetOffset(String targetUserId) {
|
||||||
final overlayContext = _overlayKey.currentContext;
|
final overlayContext = _overlayKey.currentContext;
|
||||||
final targetKey = widget.resolveTargetKey(targetUserId);
|
final targetKey = widget.resolveTargetKey(targetUserId);
|
||||||
@ -507,8 +528,11 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
return overlayBox.globalToLocal(globalTargetCenter);
|
return overlayBox.globalToLocal(globalTargetCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider<Object> _resolveImageProvider(String imagePath) {
|
ImageProvider<Object> _resolveImageProvider(
|
||||||
final currentRequest = _activeRequest ?? _centerRequest;
|
String imagePath, {
|
||||||
|
RoomGiftSeatFlightRequest? request,
|
||||||
|
}) {
|
||||||
|
final currentRequest = request ?? _activeRequest ?? _centerRequest;
|
||||||
final logicalSize =
|
final logicalSize =
|
||||||
currentRequest == null
|
currentRequest == null
|
||||||
? null
|
? null
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.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/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';
|
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||||
|
|
||||||
class LuckGiftNomorAnimWidget extends StatefulWidget {
|
class LuckGiftNomorAnimWidget extends StatefulWidget {
|
||||||
@ -17,6 +18,20 @@ class LuckGiftNomorAnimWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
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;
|
String? _currentBurstEventId;
|
||||||
bool _isRewardAmountVisible = false;
|
bool _isRewardAmountVisible = false;
|
||||||
|
|
||||||
@ -34,6 +49,48 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
|||||||
return awardAmount.toString();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
@ -45,6 +102,28 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
|||||||
_isRewardAmountVisible = false;
|
_isRewardAmountVisible = false;
|
||||||
return const SizedBox.shrink();
|
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>(
|
final rewardAnimationKey = ValueKey<String>(
|
||||||
'${rewardData.sendUserId ?? ""}'
|
'${rewardData.sendUserId ?? ""}'
|
||||||
'|${rewardData.acceptUserId ?? ""}'
|
'|${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)
|
if (_isRewardAmountVisible)
|
||||||
Positioned.fill(
|
Positioned(
|
||||||
child: Align(
|
top: awardTop,
|
||||||
alignment: const Alignment(0, 0.12),
|
left: 0,
|
||||||
child: Transform.translate(
|
right: 0,
|
||||||
offset: Offset(5.w, -4.w),
|
child: Transform.translate(
|
||||||
|
offset: Offset(awardXOffset, 0),
|
||||||
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: ScreenUtil().screenWidth * 0.56,
|
maxWidth: screenWidth * 0.34,
|
||||||
),
|
),
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: Padding(
|
child: Text(
|
||||||
padding: EdgeInsets.only(left: 0.w, right: 0.w),
|
_formatAwardAmount(rewardData.awardAmount ?? 0),
|
||||||
child: Text(
|
maxLines: 1,
|
||||||
_formatAwardAmount(rewardData.awardAmount ?? 0),
|
style: TextStyle(
|
||||||
maxLines: 1,
|
fontSize: 28.sp,
|
||||||
style: TextStyle(
|
color: const Color(0xFFFFF3B6),
|
||||||
fontSize: 28.sp,
|
fontWeight: FontWeight.w900,
|
||||||
color: const Color(0xFFFFF3B6),
|
fontStyle: FontStyle.italic,
|
||||||
fontWeight: FontWeight.w900,
|
height: 1,
|
||||||
fontStyle: FontStyle.italic,
|
shadows: const [
|
||||||
height: 1,
|
Shadow(
|
||||||
shadows: const [
|
color: Color(0xCC7A3E00),
|
||||||
Shadow(
|
blurRadius: 12,
|
||||||
color: Color(0xCC7A3E00),
|
offset: Offset(0, 3),
|
||||||
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/audio/rtc_manager.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
import 'package:yumi/modules/gift/gift_page.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/data_sources/models/enum/sc_room_roles_type.dart';
|
||||||
import '../../../shared/business_logic/models/res/sc_user_identity_res.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),
|
SizedBox(height: 2.w),
|
||||||
text(
|
SCSpecialIdBadge(
|
||||||
"ID:${ref.userCardInfo?.userProfile?.getID() ?? 0}",
|
idText:
|
||||||
fontSize: 12.sp,
|
ref
|
||||||
textColor: Colors.black,
|
.userCardInfo
|
||||||
fontWeight: FontWeight.bold,
|
?.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
|
# 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
|
# 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.
|
# 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:
|
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 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。
|
- 控制当前 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` 高成本特效播放或排队期间,暂缓低优先级的房间进场动画、全局飘屏和座位飞屏入队,待高成本特效清空后按小间隔续播,先减少多条动画链路同一时刻抢主线程的情况。
|
- 已按 2026-04-21 当前房间卡顿优化方案先落第一步“统一调度层”:新增房间特效调度器,在全屏 `VAP/SVGA` 高成本特效播放或排队期间,暂缓低优先级的房间进场动画、全局飘屏和座位飞屏入队,待高成本特效清空后按小间隔续播,先减少多条动画链路同一时刻抢主线程的情况。
|
||||||
- 本步只处理“调度时机”,没有改动现有动画内部逻辑:礼物/飞屏/飘屏的现有倍率触发规则、现有上限截断、`customAnimationCount` 现有循环方式和当前视觉表现均保持原样,避免第一步就引入联动回归。
|
- 本步只处理“调度时机”,没有改动现有动画内部逻辑:礼物/飞屏/飘屏的现有倍率触发规则、现有上限截断、`customAnimationCount` 现有循环方式和当前视觉表现均保持原样,避免第一步就引入联动回归。
|
||||||
@ -68,6 +72,8 @@
|
|||||||
- 已继续绕开幸运礼物紫色横幅里可疑的小图 `ExtendedImage` 渲染链:当前礼物图标已改为在 `initState` 里先构建 `buildCachedImageProvider`,再通过基础 `Image(image: provider)` 直接绘制,并额外输出一次 `gift icon frame ready` 日志,用于确认这张图是否真的完成首帧解码显示;这样可以把问题进一步收敛为“图片 provider/解码”还是“组件布局/视觉观感”。
|
- 已继续绕开幸运礼物紫色横幅里可疑的小图 `ExtendedImage` 渲染链:当前礼物图标已改为在 `initState` 里先构建 `buildCachedImageProvider`,再通过基础 `Image(image: provider)` 直接绘制,并额外输出一次 `gift icon frame ready` 日志,用于确认这张图是否真的完成首帧解码显示;这样可以把问题进一步收敛为“图片 provider/解码”还是“组件布局/视觉观感”。
|
||||||
- 已按 2026-04-21 最新联调请求,把紫色幸运礼物横幅 `from` 后的图标临时替换为金币图标:这一步只用于快速验证该图标位本身的布局和可见性是否正常,不再受当前礼物图资源链路影响;如果金币图标能稳定显示,就说明问题仍集中在礼物图渲染链,而不是这个位置被遮挡或根本没画出来。
|
- 已按 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 最新回归把 [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` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。
|
- 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。
|
||||||
- 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。
|
- 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。
|
||||||
- 已继续微调语言房个人卡片与送礼 UI:个人卡片底部动作文案现已支持两行居中展示,避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。
|
- 已继续微调语言房个人卡片与送礼 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/room/room_game_bottom_sheet.dart`
|
||||||
- `lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart`
|
- `lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart`
|
||||||
- `lib/modules/room/detail/room_detail_page.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/party/sc_home_party_page.dart`
|
||||||
- `lib/modules/home/popular/mine/sc_home_mine_skeleton.dart`
|
- `lib/modules/home/popular/mine/sc_home_mine_skeleton.dart`
|
||||||
- `lib/modules/home/popular/follow/sc_room_follow_page.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/modules/user/my_items/theme/bags_theme_page.dart`
|
||||||
- `lib/ui_kit/widgets/store/store_bag_page_helpers.dart`
|
- `lib/ui_kit/widgets/store/store_bag_page_helpers.dart`
|
||||||
- `sc_images/general/sc_no_data.png`
|
- `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_weekly_star_bg.png`
|
||||||
- `sc_images/splash/sc_icon_weekly_star_rank_1.png`
|
- `sc_images/splash/sc_icon_weekly_star_rank_1.png`
|
||||||
- `sc_images/splash/sc_icon_weekly_star_rank_2.png`
|
- `sc_images/splash/sc_icon_weekly_star_rank_2.png`
|
||||||
@ -419,6 +435,13 @@
|
|||||||
- Launch image 仍是默认占位资源,提交前建议替换
|
- Launch image 仍是默认占位资源,提交前建议替换
|
||||||
- `android/key.properties` 保存了本地 upload keystore 口令,必须自行妥善备份,且不要提交到仓库
|
- `android/key.properties` 保存了本地 upload keystore 口令,必须自行妥善备份,且不要提交到仓库
|
||||||
- 由于已显式移除 Agora 本地屏幕共享相关组件,如果业务后续要启用“屏幕共享/录屏推流”,需要再单独恢复相关 manifest 声明
|
- 由于已显式移除 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。
|
- 将新的 `AAB` 上传到 Play Console,并在首发流程中继续使用 Google 管理的 app signing key。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user