需求
@ -5,6 +5,7 @@ import com.android.installreferrer.api.InstallReferrerStateListener
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.TimeZone
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var installReferrerClient: InstallReferrerClient? = null
|
||||
@ -20,6 +21,15 @@ class MainActivity : FlutterActivity() {
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
MOBILE_CONTEXT_CHANNEL
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getTimeZoneIdentifier" -> result.success(TimeZone.getDefault().id)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -79,5 +89,6 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
companion object {
|
||||
private const val INSTALL_REFERRER_CHANNEL = "com.org.yumiparty/install_referrer"
|
||||
private const val MOBILE_CONTEXT_CHANNEL = "com.org.yumiparty/mobile_context"
|
||||
}
|
||||
}
|
||||
|
||||
378
docs/voice-room-big-gift-global-floating-plan.md
Normal file
@ -0,0 +1,378 @@
|
||||
# 语音房大额礼物全局飘窗方案
|
||||
|
||||
整理时间:2026-05-06
|
||||
适用范围:语音房普通大额礼物飘窗;只在一级页面 `home / explore / message / me` 展示;点击进入对应房间。
|
||||
本轮只做方案整理,不改业务代码。
|
||||
|
||||
## 1. 需求目标
|
||||
|
||||
在用户送出大额礼物时,新增一个顶部飘窗,展示类似:
|
||||
|
||||
> 用户 A 在房间 B 送了 C 礼物
|
||||
|
||||
要求:
|
||||
|
||||
- 礼物类型:普通礼物大额送礼,明确排除幸运礼物、魔法礼物等走 `giveLuckyGift` 的礼物。
|
||||
- 展示范围:全局一级页面,但只限首页主框架内的四个 Tab:`home / explore / message / me`。
|
||||
- 不在语音房页面、二级页面、设置页、充值页、聊天详情页等页面展示。
|
||||
- 点击飘窗后进入具体房间。
|
||||
- 飘窗位置沿用当前幸运礼物/礼物飘屏顶部位置。
|
||||
|
||||
## 2. 当前代码现状
|
||||
|
||||
### 2.1 顶部飘窗能力已存在
|
||||
|
||||
现有飘窗由 `OverlayManager` 统一调度:
|
||||
|
||||
- 文件:`lib/shared/data_sources/sources/local/floating_screen_manager.dart`
|
||||
- 入口:`OverlayManager().addMessage(SCFloatingMessage)`
|
||||
- 队列:`SCPriorityQueue<SCFloatingMessage>`,按 `priority` 排序。
|
||||
- 位置:`OverlayEntry` 使用 `Align(topStart)`,再 `Transform.translate(offset: Offset(0, 70.w))`。
|
||||
- 动画组件:
|
||||
- `type = 0`:`FloatingLuckGiftScreenWidget`
|
||||
- `type = 1`:`FloatingGiftScreenWidget`
|
||||
- `type = 2`:游戏中奖
|
||||
- `type = 3`:火箭
|
||||
- `type = 4`:红包
|
||||
- `type = 5`:VIP 进房
|
||||
|
||||
现有 `FloatingLuckGiftScreenWidget` 和 `FloatingGiftScreenWidget` 都已经支持:
|
||||
|
||||
- 从右往左飘过。
|
||||
- 左滑快速关闭。
|
||||
- 点击后调用 `SCRoomUtils.goRoom(roomId, context, fromFloting: true)` 进入房间。
|
||||
|
||||
### 2.2 现有普通礼物飘窗是房间内逻辑
|
||||
|
||||
普通礼物发送成功后会进入 `GiftPage.sendGiftMsg(...)`:
|
||||
|
||||
- 文件:`lib/modules/gift/gift_page.dart`
|
||||
- 当前判断:`gift.giftCandy * quantity > 9999` 时创建 `SCFloatingMessage(type: 1)`。
|
||||
- 当前内容:用户 A 送给用户 B 礼物,字段偏向 `toUserName / toUserAvatarUrl`。
|
||||
- 当前触发范围:发送者本地和当前房间 RTM 收到 `SCRoomMsgType.gift` 后触发。
|
||||
|
||||
这条逻辑不适合直接作为“全局大额礼物飘窗”:
|
||||
|
||||
- 它是房间消息,不是全服消息。
|
||||
- 它按收礼人循环,可能一次多目标送礼触发多条。
|
||||
- UI 文案是“send to 某用户”,不是“在房间 B 送了 C 礼物”。
|
||||
- `OverlayManager._shouldDisplayMessage(...)` 当前对 `type = 0 / 1 / 5` 要求必须匹配当前房间 ID,否则不展示。
|
||||
|
||||
### 2.3 全服广播通道已存在
|
||||
|
||||
RTM 初始化后会加入全服广播群:
|
||||
|
||||
- 文件:`lib/services/audio/rtm_manager.dart`
|
||||
- `init(...)` 中调用 `joinBigBroadcastGroup()`
|
||||
- 全服消息在 `_newBroadCastMsgRecv(...)` 处理
|
||||
- 当前已处理:
|
||||
- `GAME_LUCKY_GIFT`
|
||||
- 游戏中奖
|
||||
- 火箭
|
||||
- 红包
|
||||
|
||||
因此新增大额普通礼物全局飘窗时,最合适的入口是全服广播群,而不是房间群消息。
|
||||
|
||||
## 3. 推荐总体方案
|
||||
|
||||
推荐采用:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["用户在房间送普通礼物"] --> B["后端校验送礼成功"]
|
||||
B --> C{"是否普通大额礼物"}
|
||||
C -- "否" --> D["只走现有房间送礼消息"]
|
||||
C -- "是" --> E["后端推送 BIG_GIFT 到全服广播群"]
|
||||
E --> F["客户端 _newBroadCastMsgRecv 接收"]
|
||||
F --> G{"当前是否首页四个一级 Tab"}
|
||||
G -- "否" --> H["丢弃,不入队"]
|
||||
G -- "是" --> I["OverlayManager 入队展示顶部飘窗"]
|
||||
I --> J["点击 SCRoomUtils.goRoom(roomId)"]
|
||||
```
|
||||
|
||||
核心点:
|
||||
|
||||
- 大额判断放后端做,前端只负责展示,避免客户端伪造全服大礼物消息。
|
||||
- 前端新增独立消息类型,例如 `BIG_GIFT`,不复用幸运礼物 `GAME_LUCKY_GIFT`。
|
||||
- 前端新增独立飘窗类型,例如 `SCFloatingMessage.type = 6`,避免影响当前房间内 `type = 1` 的普通礼物飘窗。
|
||||
- 展示作用域由 `OverlayManager` 或主框架路由状态控制,只允许首页四 Tab 展示。
|
||||
|
||||
## 4. 服务端数据建议
|
||||
|
||||
建议后端在普通礼物接口送礼成功后判断是否需要推送全服广播:
|
||||
|
||||
- 接口链路:`/gift/batch`,前端对应 `SCChatRoomRepository.giveGift(...)`。
|
||||
- 排除链路:`/gift/give/lucky-gift`,前端对应 `giveLuckyGift(...)`。
|
||||
- 推荐阈值:沿用客户端现有房间内逻辑 `totalCoins >= 10000`,最终以产品/后端配置为准。
|
||||
- 推荐只推送一次送礼行为,不按收礼人数量重复推送。
|
||||
|
||||
推荐全服广播 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "BIG_GIFT",
|
||||
"data": {
|
||||
"eventId": "giftSendRecordId or unique id",
|
||||
"roomId": "room id",
|
||||
"roomName": "room name",
|
||||
"sendUserId": "sender user id",
|
||||
"sendUserName": "sender nickname",
|
||||
"sendUserAvatar": "sender avatar",
|
||||
"giftId": "gift id",
|
||||
"giftName": "gift name",
|
||||
"giftPhoto": "gift cover url",
|
||||
"giftQuantity": 1,
|
||||
"giftCandy": 10000,
|
||||
"totalCoins": 10000,
|
||||
"timestamp": 1710000000000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `eventId`:用于客户端去重。没有则用 `roomId + sendUserId + giftId + giftQuantity + timestamp` 做兜底 key。
|
||||
- `roomId`:点击飘窗进房必须字段。
|
||||
- `roomName`:满足“在房间 B”的展示需求。
|
||||
- `giftName / giftPhoto`:满足“送了 C 礼物”的展示和礼物图标展示。
|
||||
- `totalCoins`:可用于优先级、样式分级或埋点。
|
||||
|
||||
## 5. 前端改造点
|
||||
|
||||
### 5.1 新增大额礼物广播模型
|
||||
|
||||
建议新增模型:
|
||||
|
||||
- `lib/shared/business_logic/models/res/sc_big_gift_broadcast_push.dart`
|
||||
|
||||
字段对应服务端 payload。不要把所有字段硬塞进 `SCBroadCastLuckGiftPush`,幸运礼物模型语义不同。
|
||||
|
||||
也可以扩展 `SCFloatingMessage`:
|
||||
|
||||
- 新增 `roomName`
|
||||
- 新增 `giftName`
|
||||
- 新增 `eventId`
|
||||
- 保留 `roomId / userName / userAvatarUrl / giftUrl / giftId / number / coins / priority`
|
||||
|
||||
### 5.2 RTM 全服广播接入
|
||||
|
||||
在 `RealTimeMessagingManager._newBroadCastMsgRecv(...)` 增加:
|
||||
|
||||
```dart
|
||||
} else if (type == "BIG_GIFT") {
|
||||
final push = SCBigGiftBroadcastPush.fromJson(data);
|
||||
_handleBigGiftGlobalNews(push);
|
||||
}
|
||||
```
|
||||
|
||||
`_handleBigGiftGlobalNews(...)` 推荐职责:
|
||||
|
||||
- 校验 `roomId`、`sendUserName`、`giftName` 至少有可展示值。
|
||||
- 排除幸运礼物或服务端标记的非普通礼物。
|
||||
- 去重。
|
||||
- 组装 `SCFloatingMessage(type: 6, priority: 900, ...)`。
|
||||
- 调用 `OverlayManager().addMessage(...)`。
|
||||
|
||||
优先级建议:
|
||||
|
||||
- 幸运礼物:当前 `priority = 1000`。
|
||||
- 大额普通礼物:建议 `priority = 900`。
|
||||
- 普通房间礼物本地飘窗:保持默认或现有逻辑。
|
||||
|
||||
### 5.3 新增大额礼物飘窗 Widget
|
||||
|
||||
建议新增:
|
||||
|
||||
- `lib/ui_kit/widgets/room/floating/floating_big_gift_screen_widget.dart`
|
||||
|
||||
原因:
|
||||
|
||||
- 当前 `FloatingGiftScreenWidget` 展示的是“用户 A sendTo 用户 B + 礼物图标 x 数量”。
|
||||
- 新需求是“用户 A 在房间 B 送了 C 礼物”。
|
||||
- 新增 Widget 可以复用现有动画、点击、左滑关闭逻辑,但不影响房间内普通礼物飘窗。
|
||||
|
||||
UI 建议:
|
||||
|
||||
- 保持同一位置和相近尺寸:`height 54.w` 左右,`width 320.w - 350.w`。
|
||||
- 左侧:送礼用户头像和昵称。
|
||||
- 中间:房间名和文案,例如 `在 {roomName} 送出`。
|
||||
- 右侧:礼物图标 + 礼物名,可保留 `x{quantity}`。
|
||||
- 长昵称、长房间名、长礼物名使用 `TextOverflow.ellipsis` 或局部跑马灯,避免遮挡。
|
||||
- RTL 语言需要检查布局方向,至少保持点击和左滑逻辑与现有组件一致。
|
||||
|
||||
`OverlayManager._buildScreenWidget(...)` 增加:
|
||||
|
||||
```dart
|
||||
case 6:
|
||||
return FloatingBigGiftScreenWidget(
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
```
|
||||
|
||||
### 5.4 展示作用域:只允许首页四 Tab
|
||||
|
||||
当前 `OverlayManager` 是根 Overlay 展示,天然能覆盖全 App。需要新增“允许展示作用域”,否则可能在二级页、房间页也展示。
|
||||
|
||||
推荐方案:由 `SCIndexPage` 作为主框架控制作用域。
|
||||
|
||||
实现思路:
|
||||
|
||||
- `SCIndexPage` 接入 `RouteAware`,订阅全局 `routeObserver`。
|
||||
- 当 `SCIndexPage` 是栈顶时,设置 `OverlayManager().setMainTabsVisible(true)`。
|
||||
- 当从首页 push 到任意二级页面时,`didPushNext` 设置为 false。
|
||||
- 当从二级页返回首页时,`didPopNext` 设置为 true。
|
||||
- `SCIndexPage.dispose` 设置为 false。
|
||||
- `OverlayManager.addMessage(...)` 或 `_shouldDisplayMessage(...)` 对 `type = 6` 判断 `mainTabsVisible == true`,否则直接丢弃或不入队。
|
||||
|
||||
不推荐只用 `navigator.canPop()`:
|
||||
|
||||
- 可以作为兜底,但语义不够清晰。
|
||||
- 登录页、启动页也可能是根页面,不能等同于首页四 Tab。
|
||||
|
||||
### 5.5 调整现有展示过滤
|
||||
|
||||
当前 `OverlayManager._shouldDisplayMessage(...)` 对 `type = 0 / 1 / 5` 做当前房间匹配:
|
||||
|
||||
```dart
|
||||
if (message.type != 0 && message.type != 1 && message.type != 5) {
|
||||
return true;
|
||||
}
|
||||
...
|
||||
return currentRoomId == messageRoomId;
|
||||
```
|
||||
|
||||
新增 `type = 6` 时不要走当前房间匹配,而是走首页四 Tab 匹配:
|
||||
|
||||
- `type = 6`:只判断首页四 Tab 是否可见、全局飘屏开关是否开启、必要字段是否完整。
|
||||
- `type = 0 / 1 / 5`:保留现有房间匹配,避免破坏当前房间内特效体验。
|
||||
- `type = 2 / 3 / 4`:按现有逻辑评估是否也需要作用域限制,本需求不建议顺手改。
|
||||
|
||||
### 5.6 点击进房
|
||||
|
||||
新 Widget 点击继续复用:
|
||||
|
||||
```dart
|
||||
SCRoomUtils.goRoom(
|
||||
message.roomId!,
|
||||
navigatorKey.currentState!.context,
|
||||
fromFloting: true,
|
||||
);
|
||||
```
|
||||
|
||||
现有 `SCRoomUtils.goRoom(...)` 已处理:
|
||||
|
||||
- 当前没有房间:弹确认后进房。
|
||||
- 当前房间最小化且房间相同:打开当前房间。
|
||||
- 当前已在其他房间:弹确认切房。
|
||||
|
||||
这里不需要新增路由能力。
|
||||
|
||||
## 6. 客户端兜底方案
|
||||
|
||||
如果后端暂时不能推送 `BIG_GIFT`,可以做临时客户端兜底,但不推荐作为长期方案。
|
||||
|
||||
临时做法:
|
||||
|
||||
- 在 `GiftPage._executeGiftRequest(...)` 普通礼物成功后判断:
|
||||
- `!request.isLuckyGiftRequest`
|
||||
- `giftTab` 不是 `LUCK / LUCKY_GIFT / MAGIC`
|
||||
- `gift.giftCandy * request.quantity >= threshold`
|
||||
- 构造 `BigBroadcastGroupMessage("BIG_GIFT", SCFloatingMessage(...))`。
|
||||
- 调用 `RtmProvider.sendBigBroadcastGroup(...)`。
|
||||
- 必须只按一次送礼行为发送,不能放在 `sendGiftMsg(...)` 的 `acceptUsers` 循环里。
|
||||
|
||||
风险:
|
||||
|
||||
- 客户端可伪造全服广播,不够可信。
|
||||
- 多端并发、弱网重试可能重复。
|
||||
- 阈值和礼物类型判断容易与后端不一致。
|
||||
|
||||
因此正式方案仍建议后端推送。
|
||||
|
||||
## 7. 去重与队列策略
|
||||
|
||||
建议新增去重缓存:
|
||||
|
||||
- 位置:`RealTimeMessagingManager` 或 `OverlayManager`。
|
||||
- 结构:`LinkedHashMap<String, int>`,保存最近 100 条 event key 和时间。
|
||||
- TTL:30 秒到 60 秒。
|
||||
- key 优先使用 `eventId`。
|
||||
|
||||
队列策略:
|
||||
|
||||
- 继续使用 `OverlayManager` 现有优先级队列。
|
||||
- 大额礼物 `priority = 900`,低于幸运礼物大奖 `1000`。
|
||||
- 可选:当队列过长时丢弃低优先级大额礼物,避免首页连续刷屏。
|
||||
|
||||
## 8. 开关策略
|
||||
|
||||
沿用现有全局飘屏开关:
|
||||
|
||||
- `SCGlobalConfig.isFloatingAnimationInGlobal`
|
||||
- 本地持久化 key:`FloatingAnimationInGlobal`
|
||||
- 低性能设备会通过 `clampVisualEffectPreference(...)` 关闭。
|
||||
|
||||
建议:
|
||||
|
||||
- 大额礼物全局飘窗受这个开关控制。
|
||||
- 不受 `isGiftSpecialEffects` 控制,因为它是全屏礼物特效开关。
|
||||
- 不受 `isLuckGiftSpecialEffects` 控制,因为本需求不是幸运礼物。
|
||||
|
||||
## 9. 验收用例
|
||||
|
||||
### 9.1 基础展示
|
||||
|
||||
- 用户 A 在房间 B 送普通礼物 C,总价值达到阈值。
|
||||
- 当前用户在 `home`:看到顶部飘窗。
|
||||
- 当前用户在 `explore`:看到顶部飘窗。
|
||||
- 当前用户在 `message`:看到顶部飘窗。
|
||||
- 当前用户在 `me`:看到顶部飘窗。
|
||||
|
||||
### 9.2 范围限制
|
||||
|
||||
- 当前用户在语音房页面:不展示该全局大额礼物飘窗。
|
||||
- 当前用户在个人资料页、设置页、聊天详情页、充值页:不展示。
|
||||
- App 在启动页或登录页:不展示。
|
||||
|
||||
### 9.3 礼物类型
|
||||
|
||||
- 普通礼物达到阈值:展示。
|
||||
- 普通礼物未达到阈值:不展示。
|
||||
- 幸运礼物或魔法礼物达到金额:不走 `BIG_GIFT`,继续走现有幸运礼物逻辑。
|
||||
- 多人收礼的一次送礼:只展示一条。
|
||||
|
||||
### 9.4 点击进房
|
||||
|
||||
- 当前无房间:点击飘窗,确认后进入房间 B。
|
||||
- 当前房间已最小化且是房间 B:点击后打开当前房间。
|
||||
- 当前已在其他房间:点击后弹切房确认。
|
||||
- `roomId` 为空或房间不存在:不展示或进房失败时走现有错误提示。
|
||||
|
||||
### 9.5 稳定性
|
||||
|
||||
- 连续多条大额礼物:按队列顺序展示,不重叠。
|
||||
- 同一 `eventId` 重复到达:只展示一次。
|
||||
- 左滑关闭后下一条能继续展示。
|
||||
- 关闭全局飘屏开关后不展示。
|
||||
- 低性能设备默认不展示。
|
||||
|
||||
## 10. 建议实施顺序
|
||||
|
||||
1. 和后端确认 `BIG_GIFT` 全服广播协议、阈值、是否包含 `roomName / giftName / giftPhoto / eventId`。
|
||||
2. 新增 `SCBigGiftBroadcastPush` 模型和 `SCFloatingMessage` 扩展字段。
|
||||
3. 新增 `FloatingBigGiftScreenWidget`,复用现有动画、点击、左滑行为。
|
||||
4. `OverlayManager` 支持 `type = 6`,并新增首页四 Tab 可见性控制。
|
||||
5. `SCIndexPage` 用 `RouteAware` 控制 `OverlayManager` 的主 Tab 展示作用域。
|
||||
6. `RtmProvider._newBroadCastMsgRecv(...)` 接入 `BIG_GIFT`。
|
||||
7. 加去重和队列上限保护。
|
||||
8. 做手动验收与必要 Widget/单元测试。
|
||||
|
||||
## 11. 需要确认的问题
|
||||
|
||||
- 大额阈值最终是多少:沿用 `10000`,还是后端配置?
|
||||
- 飘窗是否展示收礼人:当前需求文案不需要,只写“用户 A 在房间 B 送了 C 礼物”。
|
||||
- 多目标送礼是否只展示一次:建议只展示一次。
|
||||
- 如果发送者自己也在首页四 Tab,是否也展示自己刚送的大额礼物:建议展示,和全服广播一致;如产品不希望自显,需要后端或前端按 `sendUserId == currentUserId` 过滤。
|
||||
- 房间 B 名称为空时的兜底文案:建议使用 `roomId` 或本地化的“语音房”。
|
||||
|
||||
@ -10,9 +10,28 @@ import UIKit
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
registerDurableAuthStorageChannel()
|
||||
registerMobileContextChannel()
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
private func registerMobileContextChannel() {
|
||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||
return
|
||||
}
|
||||
let channel = FlutterMethodChannel(
|
||||
name: "com.org.yumiparty/mobile_context",
|
||||
binaryMessenger: controller.binaryMessenger
|
||||
)
|
||||
channel.setMethodCallHandler { call, result in
|
||||
switch call.method {
|
||||
case "getTimeZoneIdentifier":
|
||||
result(TimeZone.current.identifier)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func registerDurableAuthStorageChannel() {
|
||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||
return
|
||||
|
||||
118
lib/modules/room/background/room_background_apply_service.dart
Normal file
@ -0,0 +1,118 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_history.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/room/rc_room_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_general_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
|
||||
class RoomBackgroundApplyService {
|
||||
static Future<String> apply(BuildContext context, String sourcePath) async {
|
||||
final normalizedSource = sourcePath.trim();
|
||||
if (normalizedSource.isEmpty) {
|
||||
throw StateError("Room background source is empty");
|
||||
}
|
||||
|
||||
final rtcProvider = context.read<RtcProvider>();
|
||||
final rtmProvider = context.read<RtmProvider>();
|
||||
final roomManager = context.read<SocialChatRoomManager>();
|
||||
final roomProfile = rtcProvider.currenRoom?.roomProfile?.roomProfile;
|
||||
final roomId = (roomProfile?.id ?? "").trim();
|
||||
final roomAccount = (roomProfile?.roomAccount ?? "").trim();
|
||||
if (roomId.isEmpty) {
|
||||
throw StateError("Room id is empty");
|
||||
}
|
||||
|
||||
final uploadedUrl =
|
||||
_isNetworkImage(normalizedSource)
|
||||
? normalizedSource
|
||||
: await _uploadSource(normalizedSource);
|
||||
if (uploadedUrl.trim().isEmpty) {
|
||||
throw StateError("Uploaded background URL is empty");
|
||||
}
|
||||
debugPrint(
|
||||
"[Room Background Apply] source=$normalizedSource uploadedUrl=$uploadedUrl",
|
||||
);
|
||||
|
||||
final roomInfo = await SCAccountRepository().updateRoomBackground(
|
||||
roomId,
|
||||
uploadedUrl,
|
||||
);
|
||||
final roomBackground = _preferNonEmpty(
|
||||
roomInfo.roomBackground,
|
||||
uploadedUrl,
|
||||
);
|
||||
rtcProvider.updateCurrentRoomBasicInfo(roomBackground: roomBackground);
|
||||
roomManager.updateMyRoomInfo(
|
||||
roomInfo.copyWith(
|
||||
id: roomInfo.id ?? roomId,
|
||||
roomBackground: roomBackground,
|
||||
),
|
||||
);
|
||||
if (roomAccount.isNotEmpty) {
|
||||
rtmProvider.dispatchMessage(
|
||||
Msg(
|
||||
groupId: roomAccount,
|
||||
msg: roomInfo.id ?? roomId,
|
||||
type: SCRoomMsgType.roomSettingUpdate,
|
||||
),
|
||||
addLocal: false,
|
||||
);
|
||||
}
|
||||
await RoomBackgroundHistory.add(roomBackground);
|
||||
return roomBackground;
|
||||
}
|
||||
|
||||
static bool _isNetworkImage(String value) {
|
||||
return value.startsWith("http://") || value.startsWith("https://");
|
||||
}
|
||||
|
||||
static Future<String> _uploadSource(String sourcePath) async {
|
||||
final sourceFile =
|
||||
_isAssetPath(sourcePath)
|
||||
? await _copyAssetToTemporaryFile(sourcePath)
|
||||
: File(sourcePath);
|
||||
if (!sourceFile.existsSync()) {
|
||||
throw StateError("Room background source file does not exist");
|
||||
}
|
||||
debugPrint(
|
||||
"[Room Background Upload] upload local file path=${sourceFile.path} size=${sourceFile.lengthSync()}",
|
||||
);
|
||||
final uploadedUrl = await SCGeneralRepositoryImp().upload(sourceFile);
|
||||
debugPrint("[Room Background Upload] upload success url=$uploadedUrl");
|
||||
return uploadedUrl;
|
||||
}
|
||||
|
||||
static bool _isAssetPath(String value) {
|
||||
return value.startsWith("assets/") || value.startsWith("sc_images/");
|
||||
}
|
||||
|
||||
static Future<File> _copyAssetToTemporaryFile(String assetPath) async {
|
||||
final bytes = await rootBundle.load(assetPath);
|
||||
final directory = await getTemporaryDirectory();
|
||||
final fileName = assetPath.split("/").last;
|
||||
final file = File("${directory.path}/room_background_$fileName");
|
||||
await file.writeAsBytes(
|
||||
bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),
|
||||
flush: true,
|
||||
);
|
||||
debugPrint(
|
||||
"[Room Background Upload] copied asset=$assetPath to temp=${file.path} size=${file.lengthSync()}",
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
static String _preferNonEmpty(String? primary, String fallback) {
|
||||
if ((primary ?? "").trim().isNotEmpty) {
|
||||
return primary!.trim();
|
||||
}
|
||||
return fallback.trim();
|
||||
}
|
||||
}
|
||||
14
lib/modules/room/background/room_background_assets.dart
Normal file
@ -0,0 +1,14 @@
|
||||
const List<String> roomBackgroundExampleAssets = [
|
||||
"sc_images/room/background_examples/bg_example_1.png",
|
||||
"sc_images/room/background_examples/bg_example_2.png",
|
||||
"sc_images/room/background_examples/bg_example_3.png",
|
||||
"sc_images/room/background_examples/bg_example_4.png",
|
||||
"sc_images/room/background_examples/bg_example_5.png",
|
||||
"sc_images/room/background_examples/bg_example_6.png",
|
||||
"sc_images/room/background_examples/bg_example_7.png",
|
||||
"sc_images/room/background_examples/bg_example_8.png",
|
||||
];
|
||||
|
||||
bool isRoomBackgroundExampleAsset(String value) {
|
||||
return roomBackgroundExampleAssets.contains(value.trim());
|
||||
}
|
||||
48
lib/modules/room/background/room_background_history.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
|
||||
class RoomBackgroundHistory {
|
||||
static const int _maxCount = 30;
|
||||
static const String _prefix = "room_background_history";
|
||||
|
||||
static String get _key {
|
||||
final userId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "guest";
|
||||
return "${_prefix}_$userId";
|
||||
}
|
||||
|
||||
static List<String> load() {
|
||||
final raw = DataPersistence.getString(_key);
|
||||
if (raw.trim().isEmpty) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is List) {
|
||||
return decoded
|
||||
.map((item) => item?.toString().trim() ?? "")
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
}
|
||||
} catch (_) {}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<void> add(String imageUrl) async {
|
||||
final normalized = imageUrl.trim();
|
||||
if (normalized.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final next =
|
||||
load()
|
||||
..removeWhere((item) => item == normalized)
|
||||
..insert(0, normalized);
|
||||
if (next.length > _maxCount) {
|
||||
next.removeRange(_maxCount, next.length);
|
||||
}
|
||||
await DataPersistence.setString(_key, jsonEncode(next));
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,17 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_apply_service.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_assets.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_history.dart';
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
|
||||
@ -30,13 +37,32 @@ class _RoomBackgroundSelectPageState extends State<RoomBackgroundSelectPage>
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_tabController.addListener(_handleTabChange);
|
||||
_officialItems = [
|
||||
_RoomBackgroundItem(inUse: true),
|
||||
_RoomBackgroundItem(),
|
||||
_RoomBackgroundItem(),
|
||||
_RoomBackgroundItem(),
|
||||
];
|
||||
_mineItems = [_RoomBackgroundItem(), _RoomBackgroundItem()];
|
||||
final currentBackground = _currentRoomBackground();
|
||||
_officialItems =
|
||||
roomBackgroundExampleAssets
|
||||
.map(
|
||||
(path) => _RoomBackgroundItem(
|
||||
imagePath: path,
|
||||
inUse: path == currentBackground,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final historyItems = RoomBackgroundHistory.load();
|
||||
if (currentBackground.isNotEmpty &&
|
||||
!isRoomBackgroundExampleAsset(currentBackground) &&
|
||||
!historyItems.contains(currentBackground)) {
|
||||
historyItems.insert(0, currentBackground);
|
||||
RoomBackgroundHistory.add(currentBackground);
|
||||
}
|
||||
_mineItems =
|
||||
historyItems
|
||||
.map(
|
||||
(path) => _RoomBackgroundItem(
|
||||
imagePath: path,
|
||||
inUse: path == currentBackground,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -62,35 +88,83 @@ class _RoomBackgroundSelectPageState extends State<RoomBackgroundSelectPage>
|
||||
}
|
||||
|
||||
setState(() {
|
||||
for (final item in _mineItems) {
|
||||
item.inUse = false;
|
||||
}
|
||||
_mineItems.insert(
|
||||
0,
|
||||
_RoomBackgroundItem(localImagePath: uploadedPath, inUse: true),
|
||||
);
|
||||
_markInUse(uploadedPath!);
|
||||
_upsertMineItem(uploadedPath, inUse: true);
|
||||
});
|
||||
_tabController.animateTo(1);
|
||||
}
|
||||
|
||||
void _selectItem(List<_RoomBackgroundItem> items, int index) {
|
||||
setState(() {
|
||||
for (final item in items) {
|
||||
item.inUse = false;
|
||||
}
|
||||
items[index].inUse = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _previewItem(List<_RoomBackgroundItem> items, int index) async {
|
||||
final shouldUse = await VoiceRoomRoute.openRoomBackgroundPreview<bool>(
|
||||
context,
|
||||
backgroundPath: items[index].localImagePath,
|
||||
backgroundPath: items[index].imagePath,
|
||||
);
|
||||
if (!mounted || shouldUse != true) {
|
||||
return;
|
||||
}
|
||||
_selectItem(items, index);
|
||||
await _applyItem(items, index);
|
||||
}
|
||||
|
||||
Future<void> _applyItem(List<_RoomBackgroundItem> items, int index) async {
|
||||
final item = items[index];
|
||||
final imagePath = item.imagePath.trim();
|
||||
if (imagePath.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
SCLoadingManager.show(context: context);
|
||||
try {
|
||||
final appliedUrl = await RoomBackgroundApplyService.apply(
|
||||
context,
|
||||
imagePath,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_markInUse(imagePath);
|
||||
_upsertMineItem(appliedUrl, inUse: items == _mineItems);
|
||||
});
|
||||
SCTts.show(localizations.operationSuccessful);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint("[Room Background Select] apply failed error=$e");
|
||||
debugPrint("[Room Background Select] stackTrace=$stackTrace");
|
||||
if (mounted) {
|
||||
SCTts.show(localizations.operationFail);
|
||||
}
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
String _currentRoomBackground() {
|
||||
return context
|
||||
.read<RtcProvider>()
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.roomBackground
|
||||
?.trim() ??
|
||||
"";
|
||||
}
|
||||
|
||||
void _markInUse(String imagePath) {
|
||||
final normalized = imagePath.trim();
|
||||
for (final item in [..._officialItems, ..._mineItems]) {
|
||||
item.inUse = item.imagePath == normalized;
|
||||
}
|
||||
}
|
||||
|
||||
void _upsertMineItem(String imagePath, {bool inUse = false}) {
|
||||
final normalized = imagePath.trim();
|
||||
if (normalized.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_mineItems.removeWhere((item) => item.imagePath == normalized);
|
||||
_mineItems.insert(0, _RoomBackgroundItem(imagePath: normalized));
|
||||
if (inUse) {
|
||||
_markInUse(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -371,13 +445,32 @@ class _RoomBackgroundCard extends StatelessWidget {
|
||||
|
||||
Widget _buildPreview(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
if ((item.localImagePath ?? "").isNotEmpty) {
|
||||
final file = File(item.localImagePath!);
|
||||
final imagePath = item.imagePath.trim();
|
||||
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
||||
return Image.network(
|
||||
imagePath,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _buildEmptyPreview(localizations),
|
||||
);
|
||||
}
|
||||
if (imagePath.startsWith("assets/") || imagePath.startsWith("sc_images/")) {
|
||||
return Image.asset(
|
||||
imagePath,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _buildEmptyPreview(localizations),
|
||||
);
|
||||
}
|
||||
if (imagePath.isNotEmpty) {
|
||||
final file = File(imagePath);
|
||||
if (file.existsSync()) {
|
||||
return Image.file(file, fit: BoxFit.cover);
|
||||
}
|
||||
}
|
||||
|
||||
return _buildEmptyPreview(localizations);
|
||||
}
|
||||
|
||||
Widget _buildEmptyPreview(SCAppLocalizations localizations) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@ -416,8 +509,8 @@ class _RoomBackgroundCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _RoomBackgroundItem {
|
||||
_RoomBackgroundItem({this.localImagePath, this.inUse = false});
|
||||
_RoomBackgroundItem({required this.imagePath, this.inUse = false});
|
||||
|
||||
final String? localImagePath;
|
||||
final String imagePath;
|
||||
bool inUse;
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_apply_service.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_assets.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
|
||||
@ -20,8 +24,12 @@ class RoomBackgroundUploadPage extends StatefulWidget {
|
||||
class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
File? _selectedImage;
|
||||
bool _isSaving = false;
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
if (_isSaving) {
|
||||
return;
|
||||
}
|
||||
final pickedFile = await _picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 90,
|
||||
@ -37,6 +45,42 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveBackground() async {
|
||||
final selectedImage = _selectedImage;
|
||||
if (selectedImage == null || _isSaving) {
|
||||
return;
|
||||
}
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
setState(() {
|
||||
_isSaving = true;
|
||||
});
|
||||
SCLoadingManager.show(context: context);
|
||||
try {
|
||||
final roomBackground = await RoomBackgroundApplyService.apply(
|
||||
context,
|
||||
selectedImage.path,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
SCTts.show(localizations.operationSuccessful);
|
||||
SCNavigatorUtils.goBackWithParams(context, roomBackground);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint("[Room Background Save] failed error=$e");
|
||||
debugPrint("[Room Background Save] stackTrace=$stackTrace");
|
||||
if (mounted) {
|
||||
SCTts.show(localizations.operationFail);
|
||||
}
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
@ -125,12 +169,18 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _RoomBackgroundExampleCard()),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(child: _RoomBackgroundExampleCard()),
|
||||
],
|
||||
SizedBox(
|
||||
height: 300.w,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(horizontal: 28.w),
|
||||
itemCount: roomBackgroundExampleAssets.length,
|
||||
separatorBuilder: (_, __) => SizedBox(width: 16.w),
|
||||
itemBuilder:
|
||||
(context, index) => _RoomBackgroundExampleCard(
|
||||
assetPath: roomBackgroundExampleAssets[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Text(
|
||||
@ -144,15 +194,9 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
SizedBox(height: 28.w),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap:
|
||||
_selectedImage == null
|
||||
? null
|
||||
: () => SCNavigatorUtils.goBackWithParams(
|
||||
context,
|
||||
_selectedImage!.path,
|
||||
),
|
||||
onTap: _selectedImage == null ? null : _saveBackground,
|
||||
child: Opacity(
|
||||
opacity: _selectedImage == null ? 0.45 : 1,
|
||||
opacity: _selectedImage == null || _isSaving ? 0.45 : 1,
|
||||
child: Container(
|
||||
height: 48.w,
|
||||
width: double.infinity,
|
||||
@ -188,24 +232,36 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||
}
|
||||
|
||||
class _RoomBackgroundExampleCard extends StatelessWidget {
|
||||
const _RoomBackgroundExampleCard({required this.assetPath});
|
||||
|
||||
final String assetPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = SCAppLocalizations.of(context)!;
|
||||
return Container(
|
||||
return SizedBox(
|
||||
width: 140.w,
|
||||
height: 300.w,
|
||||
decoration: BoxDecoration(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14.w),
|
||||
color: const Color(0xff143B34),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
assetPath,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(_, __, ___) => Container(
|
||||
color: const Color(0xff143B34),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
Icons.image_outlined,
|
||||
size: 42.w,
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 12.w,
|
||||
right: 12.w,
|
||||
@ -218,7 +274,7 @@ class _RoomBackgroundExampleCard extends StatelessWidget {
|
||||
color: SocialChatTheme.primaryLight,
|
||||
width: 1.w,
|
||||
),
|
||||
color: const Color(0xff1B4A40),
|
||||
color: const Color(0xff1B4A40).withValues(alpha: 0.82),
|
||||
),
|
||||
child: Text(
|
||||
localizations.staticOrGifImage,
|
||||
@ -233,6 +289,7 @@ class _RoomBackgroundExampleCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import 'package:yumi/services/gift/gift_system_manager.dart';
|
||||
import 'package:yumi/services/music/room_music_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_lucky_gift_win_sound_player.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
@ -252,7 +253,12 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
}
|
||||
}
|
||||
|
||||
String? _resolveRoomThemeBackground(JoinRoomRes? room) {
|
||||
String? _resolveRoomBackground(JoinRoomRes? room) {
|
||||
final roomBackground =
|
||||
room?.roomProfile?.roomProfile?.roomBackground?.trim() ?? "";
|
||||
if (roomBackground.isNotEmpty) {
|
||||
return roomBackground;
|
||||
}
|
||||
final roomTheme = room?.roomProps?.roomTheme;
|
||||
final themeBack = roomTheme?.themeBack ?? "";
|
||||
if (themeBack.isEmpty) {
|
||||
@ -291,11 +297,11 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
Selector<RtcProvider, String?>(
|
||||
selector:
|
||||
(context, provider) =>
|
||||
_resolveRoomThemeBackground(provider.currenRoom),
|
||||
builder: (context, roomThemeBackground, child) {
|
||||
return roomThemeBackground != null
|
||||
_resolveRoomBackground(provider.currenRoom),
|
||||
builder: (context, roomBackground, child) {
|
||||
return roomBackground != null
|
||||
? netImage(
|
||||
url: roomThemeBackground,
|
||||
url: roomBackground,
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: ScreenUtil().screenHeight,
|
||||
noDefaultImg: true,
|
||||
@ -327,7 +333,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
const RoomBottomWidget(showGiftComboButton: false),
|
||||
],
|
||||
),
|
||||
LGiftAnimalPage(),
|
||||
PositionedDirectional(
|
||||
start: 0,
|
||||
end: 0,
|
||||
top: 0,
|
||||
bottom: RoomBottomWidget.floatingButtonHostHeight.w,
|
||||
child: const IgnorePointer(child: LGiftAnimalPage()),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -20),
|
||||
child: RoomAnimationQueueScreen(),
|
||||
@ -569,6 +581,16 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
context,
|
||||
listen: false,
|
||||
).enqueueGiftAnimation(giftModel);
|
||||
unawaited(
|
||||
SCLuckyGiftWinSoundPlayer.play(
|
||||
eventKey: SCLuckyGiftWinSoundPlayer.buildEventKey(
|
||||
giftId: msg.gift?.id,
|
||||
userId: msg.user?.id,
|
||||
toUserId: msg.toUser?.id,
|
||||
awardAmount: awardAmount,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatLuckyRewardAmount(num awardAmount) {
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_preview_page.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_select_page.dart';
|
||||
import 'package:yumi/modules/room/background/room_background_upload_page.dart';
|
||||
@ -11,6 +12,13 @@ import 'package:yumi/modules/room/music/room_music_page.dart';
|
||||
import 'package:yumi/modules/room/them/room_theme_page.dart';
|
||||
import 'package:yumi/modules/room/voice_room_page.dart';
|
||||
import 'package:yumi/app/routes/sc_router_init.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart';
|
||||
|
||||
class VoiceRoomRoute implements SCIRouterProvider {
|
||||
static String voiceRoom = '/room';
|
||||
@ -51,6 +59,49 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<T?> _pushBackgroundRoute<T>(
|
||||
BuildContext context,
|
||||
Route<T> route, {
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
final restoreEffects = _pauseRoomVisualEffects(context);
|
||||
return Navigator.of(
|
||||
context,
|
||||
rootNavigator: rootNavigator,
|
||||
).push<T>(route).whenComplete(restoreEffects);
|
||||
}
|
||||
|
||||
static VoidCallback _pauseRoomVisualEffects(BuildContext context) {
|
||||
final rtcProvider = context.read<RtcProvider>();
|
||||
final rtmProvider = context.read<RtmProvider>();
|
||||
final giftAnimationManager = context.read<GiftAnimationManager>();
|
||||
final wasEnabled = rtcProvider.roomVisualEffectsEnabled;
|
||||
final floatingGiftListener = rtmProvider.msgFloatingGiftListener;
|
||||
final luckyGiftRewardTickerListener =
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener;
|
||||
|
||||
rtcProvider.setRoomVisualEffectsEnabled(false);
|
||||
rtmProvider.msgFloatingGiftListener = null;
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener = null;
|
||||
RoomEntranceHelper.clearQueue();
|
||||
giftAnimationManager.clearActiveAnimations();
|
||||
OverlayManager().removeRoom();
|
||||
SCRoomEffectScheduler().clearDeferredTasks(reason: 'room_background_route');
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
|
||||
return () {
|
||||
if (!context.mounted || rtcProvider.currenRoom == null) {
|
||||
return;
|
||||
}
|
||||
rtcProvider.setRoomVisualEffectsEnabled(wasEnabled);
|
||||
if (wasEnabled) {
|
||||
rtmProvider.msgFloatingGiftListener ??= floatingGiftListener;
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener ??=
|
||||
luckyGiftRewardTickerListener;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static Future<T?> openVoiceRoom<T>(
|
||||
BuildContext context, {
|
||||
bool replace = false,
|
||||
@ -107,11 +158,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
BuildContext context, {
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
||||
return _pushBackgroundRoute<T>(
|
||||
context,
|
||||
_buildDarkRoute<T>(
|
||||
const RoomBackgroundSelectPage(),
|
||||
settings: RouteSettings(name: roomBackgroundSelect),
|
||||
),
|
||||
rootNavigator: rootNavigator,
|
||||
);
|
||||
}
|
||||
|
||||
@ -119,11 +172,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
BuildContext context, {
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
||||
return _pushBackgroundRoute<T>(
|
||||
context,
|
||||
_buildDarkRoute<T>(
|
||||
const RoomBackgroundUploadPage(),
|
||||
settings: RouteSettings(name: roomBackgroundUpload),
|
||||
),
|
||||
rootNavigator: rootNavigator,
|
||||
);
|
||||
}
|
||||
|
||||
@ -132,11 +187,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
String? backgroundPath,
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
||||
return _pushBackgroundRoute<T>(
|
||||
context,
|
||||
_buildDarkRoute<T>(
|
||||
RoomBackgroundPreviewPage(backgroundPath: backgroundPath),
|
||||
settings: RouteSettings(name: roomBackgroundPreview),
|
||||
),
|
||||
rootNavigator: rootNavigator,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:ui';
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/modules/user/profile/profile/sc_profile_page.dart';
|
||||
@ -28,6 +29,7 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
|
||||
import '../../../app/constants/sc_screen.dart';
|
||||
@ -157,7 +159,6 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
||||
}
|
||||
|
||||
Widget _buildTab(int index, String text) {
|
||||
final isSelected = _tabController.index == index;
|
||||
return Tab(text: text);
|
||||
}
|
||||
|
||||
@ -168,6 +169,236 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildHeaderAvatar(SocialChatUserProfileManager ref) {
|
||||
if (ref.userProfile?.cpList?.isNotEmpty ?? false) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 100.w,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 170.w,
|
||||
height: 100.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(0, -5.w),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: netImage(
|
||||
url:
|
||||
ref.userProfile?.cpList?.first.meUserAvatar ?? "",
|
||||
defaultImg:
|
||||
"sc_images/general/sc_icon_avar_defalt.png",
|
||||
width: 56.w,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
onTap: () {
|
||||
String encodedUrls = Uri.encodeComponent(
|
||||
jsonEncode([
|
||||
ref.userProfile?.cpList?.first.meUserAvatar,
|
||||
]),
|
||||
);
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0",
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(width: 32.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
replace: true,
|
||||
"${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == ref.userProfile?.cpList?.first.cpUserId}&tageId=${ref.userProfile?.cpList?.first.cpUserId}",
|
||||
);
|
||||
},
|
||||
child: netImage(
|
||||
url:
|
||||
ref.userProfile?.cpList?.first.cpUserAvatar ?? "",
|
||||
defaultImg:
|
||||
"sc_images/general/sc_icon_avar_defalt.png",
|
||||
width: 56.w,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -15.w),
|
||||
child: Image.asset(
|
||||
"sc_images/person/sc_icon_send_cp_requst_dialog_head.png",
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
child: head(
|
||||
url: ref.userProfile?.userAvatar ?? "",
|
||||
width: 88.w,
|
||||
headdress: ref.userProfile?.getHeaddress()?.sourceUrl,
|
||||
),
|
||||
onTap: () {
|
||||
String encodedUrls = Uri.encodeComponent(
|
||||
jsonEncode([ref.userProfile?.userAvatar]),
|
||||
);
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderName(SocialChatUserProfileManager ref) {
|
||||
final nickname = ref.userProfile?.userNickname ?? "";
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
socialchatNickNameText(
|
||||
nickname,
|
||||
maxWidth: 240.w,
|
||||
fontSize: 18.sp,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
type: ref.userProfile?.getVIP()?.name ?? "",
|
||||
needScroll: nickname.characters.length > 18,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
getVIPBadge(ref.userProfile?.getVIP()?.name, width: 45.w, height: 25.w),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderMeta(SocialChatUserProfileManager ref) {
|
||||
final idText = ref.userProfile?.getID() ?? "";
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: idText));
|
||||
SCTts.show(SCAppLocalizations.of(context)!.copiedToClipboard);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SCSpecialIdBadge(
|
||||
idText: idText,
|
||||
showAnimated: ref.userProfile?.hasSpecialId() ?? false,
|
||||
assetPath: SCSpecialIdAssets.userIdLarge,
|
||||
animationWidth: 80.w,
|
||||
animationHeight: 32.w,
|
||||
showTextBesideAnimated: true,
|
||||
animatedTextSpacing: 0,
|
||||
showAnimatedGradientText:
|
||||
ref.userProfile?.shouldShowColoredSpecialIdText() ??
|
||||
false,
|
||||
animationTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
normalTextStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
animationFit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Image.asset(
|
||||
"sc_images/room/sc_icon_user_card_copy_id.png",
|
||||
width: 15.w,
|
||||
height: 15.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 9.w),
|
||||
_buildHeaderGenderAge(ref),
|
||||
SizedBox(width: 8.w),
|
||||
_buildHeaderCountryFlag(ref),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderGenderAge(SocialChatUserProfileManager ref) {
|
||||
return Container(
|
||||
width: (ref.userProfile?.age ?? 0) > 999 ? 58.w : 48.w,
|
||||
height: 24.w,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
ref.userProfile?.userSex == 0
|
||||
? "sc_images/login/sc_icon_sex_woman_bg.png"
|
||||
: "sc_images/login/sc_icon_sex_man_bg.png",
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
xb(ref.userProfile?.userSex),
|
||||
text(
|
||||
"${ref.userProfile?.age ?? 0}",
|
||||
textColor: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderCountryFlag(SocialChatUserProfileManager ref) {
|
||||
final countryName = ref.userProfile?.countryName ?? "";
|
||||
String flag = "";
|
||||
try {
|
||||
flag =
|
||||
Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).findCountryByName(countryName)?.nationalFlag ??
|
||||
"";
|
||||
} catch (_) {
|
||||
flag = "";
|
||||
}
|
||||
if (flag.isEmpty) return const SizedBox.shrink();
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2.w),
|
||||
child: netImage(
|
||||
url: flag,
|
||||
width: 24.w,
|
||||
height: 16.w,
|
||||
fit: BoxFit.cover,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileHeader(
|
||||
SocialChatUserProfileManager ref,
|
||||
List<PersonPhoto> backgroundPhotos,
|
||||
@ -208,7 +439,7 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
||||
}).toList(),
|
||||
)
|
||||
: Image.asset(
|
||||
'sc_images/person/sc_icon_my_head_bg_defalt.png',
|
||||
'sc_images/person/sc_icon_profile_card_default_bg.png',
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: 300.w,
|
||||
fit: BoxFit.cover,
|
||||
@ -219,212 +450,20 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff083b2f),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8.w),
|
||||
topRight: Radius.circular(8.w),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Container(color: Color(0xff083b2f)),
|
||||
),
|
||||
Positioned(
|
||||
top: 210.w,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 15.w),
|
||||
(ref.userProfile?.cpList?.isNotEmpty ?? false)
|
||||
? SizedBox(
|
||||
height: 100.w,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(0, -5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: netImage(
|
||||
url:
|
||||
ref
|
||||
.userProfile
|
||||
?.cpList
|
||||
?.first
|
||||
.meUserAvatar ??
|
||||
"",
|
||||
defaultImg:
|
||||
"sc_images/general/sc_icon_avar_defalt.png",
|
||||
width: 56.w,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
onTap: () {
|
||||
String encodedUrls =
|
||||
Uri.encodeComponent(
|
||||
jsonEncode([
|
||||
ref
|
||||
.userProfile
|
||||
?.cpList
|
||||
?.first
|
||||
.meUserAvatar,
|
||||
]),
|
||||
);
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0",
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(width: 32.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
replace: true,
|
||||
"${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == ref.userProfile?.cpList?.first.cpUserId}&tageId=${ref.userProfile?.cpList?.first.cpUserId}",
|
||||
);
|
||||
},
|
||||
child: netImage(
|
||||
url:
|
||||
ref
|
||||
.userProfile
|
||||
?.cpList
|
||||
?.first
|
||||
.cpUserAvatar ??
|
||||
"",
|
||||
defaultImg:
|
||||
"sc_images/general/sc_icon_avar_defalt.png",
|
||||
width: 56.w,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -15),
|
||||
child: Image.asset(
|
||||
"sc_images/person/sc_icon_send_cp_requst_dialog_head.png",
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: GestureDetector(
|
||||
child: head(
|
||||
url: ref.userProfile?.userAvatar ?? "",
|
||||
width: 88.w,
|
||||
headdress:
|
||||
ref.userProfile?.getHeaddress()?.sourceUrl,
|
||||
),
|
||||
onTap: () {
|
||||
String encodedUrls = Uri.encodeComponent(
|
||||
jsonEncode([ref.userProfile?.userAvatar]),
|
||||
);
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0",
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildHeaderAvatar(ref),
|
||||
SizedBox(height: 6.w),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 25.w),
|
||||
socialchatNickNameText(
|
||||
maxWidth: 135.w,
|
||||
ref.userProfile?.userNickname ?? "",
|
||||
fontSize: 18.sp,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
type: ref.userProfile?.getVIP()?.name ?? "",
|
||||
needScroll:
|
||||
(ref.userProfile?.userNickname?.characters.length ??
|
||||
0) >
|
||||
13,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
getVIPBadge(
|
||||
ref.userProfile?.getVIP()?.name,
|
||||
width: 45.w,
|
||||
height: 25.w,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 5.w),
|
||||
Container(
|
||||
width:
|
||||
(ref.userProfile?.age ?? 0) > 999 ? 58.w : 48.w,
|
||||
height: 24.w,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
ref.userProfile?.userSex == 0
|
||||
? "sc_images/login/sc_icon_sex_woman_bg.png"
|
||||
: "sc_images/login/sc_icon_sex_man_bg.png",
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
xb(ref.userProfile?.userSex),
|
||||
text(
|
||||
"${ref.userProfile?.age}",
|
||||
textColor: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildHeaderName(ref),
|
||||
SizedBox(height: 5.w),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(width: 25.w),
|
||||
SCSpecialIdBadge(
|
||||
idText: ref.userProfile?.getID() ?? "",
|
||||
showAnimated: ref.userProfile?.hasSpecialId() ?? false,
|
||||
assetPath: SCSpecialIdAssets.userIdLarge,
|
||||
animationWidth: 80.w,
|
||||
animationHeight: 32.w,
|
||||
showTextBesideAnimated: true,
|
||||
animatedTextSpacing: 0,
|
||||
showAnimatedGradientText:
|
||||
ref.userProfile?.shouldShowColoredSpecialIdText() ??
|
||||
false,
|
||||
animationTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
normalTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
animationFit: BoxFit.contain,
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildHeaderMeta(ref),
|
||||
SizedBox(height: 5.w),
|
||||
Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, ref, child) {
|
||||
@ -560,127 +599,6 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkeletonPiece({
|
||||
double? width,
|
||||
required double height,
|
||||
BorderRadius? borderRadius,
|
||||
BoxShape shape = BoxShape.rectangle,
|
||||
}) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.14),
|
||||
borderRadius:
|
||||
shape == BoxShape.circle
|
||||
? null
|
||||
: (borderRadius ?? BorderRadius.circular(8.w)),
|
||||
shape: shape,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileSkeleton() {
|
||||
return SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 210.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 15.w),
|
||||
child: _buildSkeletonPiece(
|
||||
width: 88.w,
|
||||
height: 88.w,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 25.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSkeletonPiece(width: 150.w, height: 24.w),
|
||||
SizedBox(height: 10.w),
|
||||
_buildSkeletonPiece(width: 110.w, height: 18.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
Container(
|
||||
height: 72.w,
|
||||
alignment: AlignmentDirectional.center,
|
||||
width: ScreenUtil().screenWidth,
|
||||
margin: EdgeInsets.symmetric(horizontal: 18.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 35.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(
|
||||
3,
|
||||
(_) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildSkeletonPiece(width: 24.w, height: 22.w),
|
||||
SizedBox(height: 8.w),
|
||||
_buildSkeletonPiece(width: 58.w, height: 14.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 3.w),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xff083b2f), Color(0xff083b2f)],
|
||||
begin: AlignmentDirectional.topStart,
|
||||
end: AlignmentDirectional.bottomEnd,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 10.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildSkeletonPiece(width: 92.w, height: 18.w),
|
||||
SizedBox(width: 14.w),
|
||||
_buildSkeletonPiece(width: 72.w, height: 18.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSkeletonPiece(width: 100.w, height: 22.w),
|
||||
SizedBox(height: 18.w),
|
||||
_buildSkeletonPiece(width: 220.w, height: 16.w),
|
||||
SizedBox(height: 14.w),
|
||||
_buildSkeletonPiece(width: 190.w, height: 16.w),
|
||||
SizedBox(height: 14.w),
|
||||
_buildSkeletonPiece(width: 240.w, height: 16.w),
|
||||
SizedBox(height: 14.w),
|
||||
_buildSkeletonPiece(width: 200.w, height: 16.w),
|
||||
SizedBox(height: 14.w),
|
||||
_buildSkeletonPiece(width: 160.w, height: 16.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 220.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_tabs.clear();
|
||||
@ -722,7 +640,7 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Image.asset(
|
||||
'sc_images/person/sc_icon_my_head_bg_defalt.png',
|
||||
'sc_images/person/sc_icon_profile_card_default_bg.png',
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: 300.w,
|
||||
fit: BoxFit.cover,
|
||||
@ -1063,7 +981,7 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
||||
isBlacklist
|
||||
? Container()
|
||||
: (isProfileLoading || isBlacklistLoading
|
||||
? _buildProfileSkeleton()
|
||||
? const SizedBox.shrink()
|
||||
: ExtendedNestedScrollView(
|
||||
controller: _scrollController,
|
||||
onlyOneScrollInBody: true,
|
||||
|
||||
@ -517,7 +517,9 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text(
|
||||
config?.displayNameText ?? 'VIP$level',
|
||||
style: TextStyle(
|
||||
color:
|
||||
@ -530,6 +532,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
SizedBox(
|
||||
width: 0,
|
||||
@ -1147,7 +1150,9 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text(
|
||||
_priceText,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFFE8A7),
|
||||
@ -1155,6 +1160,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SCDebounceWidget(
|
||||
onTap: canPurchase ? _purchaseSelectedLevel : () {},
|
||||
@ -1208,6 +1214,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@ -1234,6 +1242,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
_buildVipLevelNumber(level),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1362,7 +1371,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
preview?.payableGold ?? config?.payableGold ?? config?.priceGold;
|
||||
final days = (preview?.durationDays ?? config?.durationDays ?? 30).toInt();
|
||||
final amountText = amount == null ? '--' : _formatGold(amount);
|
||||
return '$amountText / $days Days';
|
||||
final daysLabel = SCAppLocalizations.of(context)?.days ?? 'Days';
|
||||
return '$amountText / $days $daysLabel';
|
||||
}
|
||||
|
||||
String _formatGold(num value) {
|
||||
|
||||
@ -99,6 +99,7 @@ class RoomEntryPreviewData {
|
||||
this.roomAccount,
|
||||
this.userId,
|
||||
this.roomCover,
|
||||
this.roomBackground,
|
||||
this.roomName,
|
||||
this.roomDesc,
|
||||
this.event,
|
||||
@ -124,6 +125,7 @@ class RoomEntryPreviewData {
|
||||
roomAccount: room.roomAccount,
|
||||
userId: room.userId,
|
||||
roomCover: room.roomCover,
|
||||
roomBackground: room.roomBackground,
|
||||
roomName: room.roomName,
|
||||
roomDesc: room.roomDesc,
|
||||
event: room.event,
|
||||
@ -145,6 +147,7 @@ class RoomEntryPreviewData {
|
||||
roomAccount: roomProfile?.roomAccount,
|
||||
userId: roomProfile?.userId,
|
||||
roomCover: roomProfile?.roomCover,
|
||||
roomBackground: roomProfile?.roomBackground,
|
||||
roomName: roomProfile?.roomName,
|
||||
roomDesc: roomProfile?.roomDesc,
|
||||
event: roomProfile?.event,
|
||||
@ -168,6 +171,7 @@ class RoomEntryPreviewData {
|
||||
roomAccount: room.roomAccount,
|
||||
userId: room.userId,
|
||||
roomCover: room.roomCover,
|
||||
roomBackground: room.roomBackground,
|
||||
roomName: room.roomName,
|
||||
roomDesc: room.roomDesc,
|
||||
event: room.event,
|
||||
@ -192,6 +196,7 @@ class RoomEntryPreviewData {
|
||||
final String? roomAccount;
|
||||
final String? userId;
|
||||
final String? roomCover;
|
||||
final String? roomBackground;
|
||||
final String? roomName;
|
||||
final String? roomDesc;
|
||||
final String? event;
|
||||
@ -241,6 +246,7 @@ class RoomEntryPreviewData {
|
||||
nationalFlag: nationalFlag,
|
||||
roomAccount: roomAccount,
|
||||
roomCover: roomCover,
|
||||
roomBackground: roomBackground,
|
||||
roomDesc: roomDesc,
|
||||
roomName: roomName,
|
||||
sysOrigin: sysOrigin,
|
||||
@ -1964,6 +1970,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
void updateCurrentRoomBasicInfo({
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
}) {
|
||||
@ -1975,6 +1982,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
roomProfile: currenRoom?.roomProfile?.copyWith(
|
||||
roomProfile: currentRoomProfile.copyWith(
|
||||
roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover),
|
||||
roomBackground: _preferNonEmpty(
|
||||
roomBackground,
|
||||
currentRoomProfile.roomBackground,
|
||||
),
|
||||
roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName),
|
||||
roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc),
|
||||
),
|
||||
@ -1983,6 +1994,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
SCRoomProfileCache.saveRoomProfile(
|
||||
roomId: currentRoomProfile.id ?? "",
|
||||
roomCover: roomCover,
|
||||
roomBackground: roomBackground,
|
||||
roomName: roomName,
|
||||
roomDesc: roomDesc,
|
||||
);
|
||||
@ -3601,7 +3613,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final value = await SCChatRoomRepository().specific(roomId);
|
||||
final currentRoomProfile = currenRoom?.roomProfile?.roomProfile;
|
||||
debugPrint(
|
||||
"[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomName=${value.roomName ?? ""}",
|
||||
"[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomBackground=${value.roomBackground ?? ""} roomName=${value.roomName ?? ""}",
|
||||
);
|
||||
currenRoom = currenRoom?.copyWith(
|
||||
roomProfile: currenRoom?.roomProfile?.copyWith(
|
||||
@ -3612,6 +3624,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
value.roomCover,
|
||||
currentRoomProfile.roomCover,
|
||||
),
|
||||
roomBackground: _preferNonEmpty(
|
||||
value.roomBackground,
|
||||
currentRoomProfile.roomBackground,
|
||||
),
|
||||
roomName: _preferNonEmpty(
|
||||
value.roomName,
|
||||
currentRoomProfile.roomName,
|
||||
@ -3624,7 +3640,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
),
|
||||
);
|
||||
debugPrint(
|
||||
"[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""}",
|
||||
"[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""} roomBackground=${currenRoom?.roomProfile?.roomProfile?.roomBackground ?? ""}",
|
||||
);
|
||||
|
||||
///如果是游客禁止上麦,已经在麦上的游客需要下麦
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
|
||||
|
||||
class GiftAnimationManager extends ChangeNotifier {
|
||||
static const int maxVisibleSlots = 4;
|
||||
static const int _maxPendingAnimations = 24;
|
||||
static const Duration _activeComboIdleDismissDelay = Duration(
|
||||
milliseconds: 3200,
|
||||
@ -17,10 +18,16 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
//每个控件正在播放的动画
|
||||
Map<int, LGiftModel?> giftMap = {0: null, 1: null, 2: null, 3: null};
|
||||
|
||||
int _visibleSlotCount = 0;
|
||||
|
||||
bool get _controllersReady =>
|
||||
animationControllerList.length >= giftMap.length;
|
||||
_visibleSlotCount <= 0 ||
|
||||
animationControllerList.length >= _visibleSlotCount;
|
||||
|
||||
GiftAnimationSlotSnapshot? slotSnapshotAt(int index) {
|
||||
if (index < 0 || index >= _visibleSlotCount) {
|
||||
return null;
|
||||
}
|
||||
final gift = giftMap[index];
|
||||
if (gift == null || animationControllerList.length <= index) {
|
||||
return null;
|
||||
@ -43,6 +50,31 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void setVisibleSlotCount(int count) {
|
||||
final nextCount = count.clamp(0, maxVisibleSlots).toInt();
|
||||
if (nextCount == _visibleSlotCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
final oldCount = _visibleSlotCount;
|
||||
if (nextCount < oldCount) {
|
||||
for (var index = oldCount - 1; index >= nextCount; index -= 1) {
|
||||
final gift = giftMap[index];
|
||||
if (gift == null) {
|
||||
continue;
|
||||
}
|
||||
_cancelSlotDismissTimer(index);
|
||||
giftMap[index] = null;
|
||||
pendingAnimationsQueue.addFirst(gift);
|
||||
}
|
||||
_trimPendingAnimationsFromEnd();
|
||||
}
|
||||
|
||||
_visibleSlotCount = nextCount;
|
||||
notifyListeners();
|
||||
proceedToNextAnimation();
|
||||
}
|
||||
|
||||
void enqueueGiftAnimation(LGiftModel giftModel) {
|
||||
if (_mergeIntoActiveAnimation(giftModel)) {
|
||||
return;
|
||||
@ -55,21 +87,42 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
proceedToNextAnimation();
|
||||
}
|
||||
|
||||
void clearActiveAnimations() {
|
||||
pendingAnimationsQueue.clear();
|
||||
var changed = false;
|
||||
for (final index in giftMap.keys.toList()) {
|
||||
_cancelSlotDismissTimer(index);
|
||||
if (giftMap[index] != null) {
|
||||
giftMap[index] = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _trimPendingAnimations() {
|
||||
while (pendingAnimationsQueue.length >= _maxPendingAnimations) {
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
void _trimPendingAnimationsFromEnd() {
|
||||
while (pendingAnimationsQueue.length > _maxPendingAnimations) {
|
||||
pendingAnimationsQueue.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
bool _mergeIntoActiveAnimation(LGiftModel incoming) {
|
||||
for (final entry in giftMap.entries) {
|
||||
final current = entry.value;
|
||||
for (var index = 0; index < _visibleSlotCount; index += 1) {
|
||||
final current = giftMap[index];
|
||||
if (current == null || current.labelId != incoming.labelId) {
|
||||
continue;
|
||||
}
|
||||
_mergeGiftModel(target: current, incoming: incoming);
|
||||
notifyListeners();
|
||||
_refreshSlotAnimation(entry.key, restartEntry: false);
|
||||
_refreshSlotAnimation(index, restartEntry: false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -119,41 +172,60 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
|
||||
///开始播放
|
||||
proceedToNextAnimation() {
|
||||
if (pendingAnimationsQueue.isEmpty || !_controllersReady) {
|
||||
if (pendingAnimationsQueue.isEmpty ||
|
||||
!_controllersReady ||
|
||||
_visibleSlotCount <= 0) {
|
||||
return;
|
||||
}
|
||||
var playGift = pendingAnimationsQueue.first;
|
||||
for (var key in giftMap.keys) {
|
||||
var value = giftMap[key];
|
||||
|
||||
var changed = false;
|
||||
while (pendingAnimationsQueue.isNotEmpty) {
|
||||
final playGift = pendingAnimationsQueue.first;
|
||||
var consumed = false;
|
||||
for (var index = 0; index < _visibleSlotCount; index += 1) {
|
||||
final value = giftMap[index];
|
||||
if (value == null) {
|
||||
giftMap[key] = playGift;
|
||||
giftMap[index] = playGift;
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
notifyListeners();
|
||||
_refreshSlotAnimation(key, restartEntry: true);
|
||||
changed = true;
|
||||
consumed = true;
|
||||
_refreshSlotAnimation(index, restartEntry: true);
|
||||
break;
|
||||
} else {
|
||||
}
|
||||
if (value.labelId == playGift.labelId) {
|
||||
_mergeGiftModel(target: value, incoming: playGift);
|
||||
pendingAnimationsQueue.removeFirst();
|
||||
notifyListeners();
|
||||
_refreshSlotAnimation(key, restartEntry: false);
|
||||
changed = true;
|
||||
consumed = true;
|
||||
_refreshSlotAnimation(index, restartEntry: false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!consumed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void attachAnimationControllers(List<LGiftScrollingScreenAnimsBean> anins) {
|
||||
animationControllerList = anins;
|
||||
if (_visibleSlotCount > animationControllerList.length) {
|
||||
setVisibleSlotCount(animationControllerList.length);
|
||||
return;
|
||||
}
|
||||
proceedToNextAnimation();
|
||||
}
|
||||
|
||||
void cleanupAnimationResources() {
|
||||
pendingAnimationsQueue.clear();
|
||||
giftMap[0] = null;
|
||||
giftMap[1] = null;
|
||||
giftMap[2] = null;
|
||||
giftMap[3] = null;
|
||||
for (final key in giftMap.keys) {
|
||||
giftMap[key] = null;
|
||||
}
|
||||
_visibleSlotCount = 0;
|
||||
for (var element in animationControllerList) {
|
||||
element.dismissTimer?.cancel();
|
||||
element.controller.dispose();
|
||||
|
||||
@ -43,6 +43,9 @@ class SocialChatRoomManager extends ChangeNotifier {
|
||||
if ((roomInfo.roomCover ?? "").trim().isNotEmpty) {
|
||||
myRoom?.setRoomCover = roomInfo.roomCover!;
|
||||
}
|
||||
if ((roomInfo.roomBackground ?? "").trim().isNotEmpty) {
|
||||
myRoom?.setRoomBackground = roomInfo.roomBackground!;
|
||||
}
|
||||
if (roomInfo.roomName != null) {
|
||||
myRoom?.setRoomName = roomInfo.roomName!;
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ class RoomProfile {
|
||||
String? roomAccount,
|
||||
List<String>? roomBadgeIcons,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomGameIcon,
|
||||
String? roomName,
|
||||
@ -85,6 +86,7 @@ class RoomProfile {
|
||||
_roomAccount = roomAccount;
|
||||
_roomBadgeIcons = roomBadgeIcons;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomDesc = roomDesc;
|
||||
_roomGameIcon = roomGameIcon;
|
||||
_roomName = roomName;
|
||||
@ -114,6 +116,7 @@ class RoomProfile {
|
||||
? json['roomBadgeIcons'].cast<String>()
|
||||
: [];
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_roomGameIcon = json['roomGameIcon'];
|
||||
_roomName = json['roomName'];
|
||||
@ -146,6 +149,7 @@ class RoomProfile {
|
||||
String? _roomAccount;
|
||||
List<String>? _roomBadgeIcons;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomDesc;
|
||||
String? _roomGameIcon;
|
||||
String? _roomName;
|
||||
@ -166,6 +170,7 @@ class RoomProfile {
|
||||
String? roomAccount,
|
||||
List<String>? roomBadgeIcons,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomGameIcon,
|
||||
String? roomName,
|
||||
@ -186,6 +191,7 @@ class RoomProfile {
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
||||
roomName: roomName ?? _roomName,
|
||||
@ -206,6 +212,7 @@ class RoomProfile {
|
||||
String? get roomAccount => _roomAccount;
|
||||
List<String>? get roomBadgeIcons => _roomBadgeIcons;
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
String? get roomDesc => _roomDesc;
|
||||
String? get roomGameIcon => _roomGameIcon;
|
||||
String? get roomName => _roomName;
|
||||
@ -243,6 +250,7 @@ class RoomProfile {
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['roomBadgeIcons'] = _roomBadgeIcons;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['roomGameIcon'] = _roomGameIcon;
|
||||
map['roomName'] = _roomName;
|
||||
|
||||
@ -716,6 +716,7 @@ class RoomProfile2 {
|
||||
String? nationalFlag,
|
||||
String? roomAccount,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomName,
|
||||
String? sysOrigin,
|
||||
@ -733,6 +734,7 @@ class RoomProfile2 {
|
||||
_nationalFlag = nationalFlag;
|
||||
_roomAccount = roomAccount;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomDesc = roomDesc;
|
||||
_roomName = roomName;
|
||||
_sysOrigin = sysOrigin;
|
||||
@ -752,6 +754,7 @@ class RoomProfile2 {
|
||||
_nationalFlag = json['nationalFlag'];
|
||||
_roomAccount = json['roomAccount'];
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_roomName = json['roomName'];
|
||||
_sysOrigin = json['sysOrigin'];
|
||||
@ -770,6 +773,7 @@ class RoomProfile2 {
|
||||
String? _nationalFlag;
|
||||
String? _roomAccount;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomDesc;
|
||||
String? _roomName;
|
||||
String? _sysOrigin;
|
||||
@ -788,6 +792,7 @@ class RoomProfile2 {
|
||||
String? nationalFlag,
|
||||
String? roomAccount,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomName,
|
||||
String? sysOrigin,
|
||||
@ -805,6 +810,7 @@ class RoomProfile2 {
|
||||
nationalFlag: nationalFlag ?? _nationalFlag,
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
roomName: roomName ?? _roomName,
|
||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||
@ -833,6 +839,7 @@ class RoomProfile2 {
|
||||
String? get roomAccount => _roomAccount;
|
||||
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
|
||||
String? get roomDesc => _roomDesc;
|
||||
|
||||
@ -857,6 +864,7 @@ class RoomProfile2 {
|
||||
map['nationalFlag'] = _nationalFlag;
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['roomName'] = _roomName;
|
||||
map['sysOrigin'] = _sysOrigin;
|
||||
|
||||
@ -29,6 +29,7 @@ class MyRoomRes {
|
||||
String? roomAccount,
|
||||
String? userId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
String? event,
|
||||
@ -49,6 +50,7 @@ class MyRoomRes {
|
||||
_roomAccount = roomAccount;
|
||||
_userId = userId;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomName = roomName;
|
||||
_roomDesc = roomDesc;
|
||||
_event = event;
|
||||
@ -71,6 +73,7 @@ class MyRoomRes {
|
||||
_roomAccount = json['roomAccount'];
|
||||
_userId = json['userId'];
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomName = json['roomName'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_event = json['event'];
|
||||
@ -98,6 +101,7 @@ class MyRoomRes {
|
||||
String? _roomAccount;
|
||||
String? _userId;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomName;
|
||||
String? _roomDesc;
|
||||
String? _event;
|
||||
@ -118,6 +122,7 @@ class MyRoomRes {
|
||||
String? roomAccount,
|
||||
String? userId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
String? event,
|
||||
@ -138,6 +143,7 @@ class MyRoomRes {
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
userId: userId ?? _userId,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomName: roomName ?? _roomName,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
event: event ?? _event,
|
||||
@ -158,6 +164,7 @@ class MyRoomRes {
|
||||
String? get roomAccount => _roomAccount;
|
||||
String? get userId => _userId;
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
String? get roomName => _roomName;
|
||||
String? get roomDesc => _roomDesc;
|
||||
String? get event => _event;
|
||||
@ -174,6 +181,7 @@ class MyRoomRes {
|
||||
RoomCounter? get counter => _counter;
|
||||
List<UseBadges>? get useBadges => _useBadges;
|
||||
set setRoomCover(String value) => _roomCover = value;
|
||||
set setRoomBackground(String value) => _roomBackground = value;
|
||||
set setRoomName(String value) => _roomName = value;
|
||||
set setRoomDesc(String value) => _roomDesc = value;
|
||||
|
||||
@ -183,6 +191,7 @@ class MyRoomRes {
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['userId'] = _userId;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomName'] = _roomName;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['event'] = _event;
|
||||
|
||||
@ -30,6 +30,7 @@ class SocialChatRoomRes {
|
||||
String? roomAccount,
|
||||
List<String>? roomBadgeIcons,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomGameIcon,
|
||||
String? roomName,
|
||||
@ -50,6 +51,7 @@ class SocialChatRoomRes {
|
||||
_roomAccount = roomAccount;
|
||||
_roomBadgeIcons = roomBadgeIcons;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomDesc = roomDesc;
|
||||
_roomGameIcon = roomGameIcon;
|
||||
_roomName = roomName;
|
||||
@ -81,6 +83,7 @@ class SocialChatRoomRes {
|
||||
.toList()
|
||||
: null;
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_roomGameIcon = json['roomGameIcon'];
|
||||
_roomName = json['roomName'];
|
||||
@ -114,6 +117,7 @@ class SocialChatRoomRes {
|
||||
String? _roomAccount;
|
||||
List<String>? _roomBadgeIcons;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomDesc;
|
||||
String? _roomGameIcon;
|
||||
String? _roomName;
|
||||
@ -135,6 +139,7 @@ class SocialChatRoomRes {
|
||||
String? roomAccount,
|
||||
List<String>? roomBadgeIcons,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomDesc,
|
||||
String? roomGameIcon,
|
||||
String? roomName,
|
||||
@ -155,6 +160,7 @@ class SocialChatRoomRes {
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
||||
roomName: roomName ?? _roomName,
|
||||
@ -185,6 +191,7 @@ class SocialChatRoomRes {
|
||||
List<String>? get roomBadgeIcons => _roomBadgeIcons;
|
||||
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
|
||||
String? get roomDesc => _roomDesc;
|
||||
|
||||
@ -228,6 +235,7 @@ class SocialChatRoomRes {
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['roomBadgeIcons'] = _roomBadgeIcons;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['roomGameIcon'] = _roomGameIcon;
|
||||
map['roomName'] = _roomName;
|
||||
|
||||
@ -21,6 +21,7 @@ class SCEditRoomInfoRes {
|
||||
String? roomAccount,
|
||||
String? userId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
String? event,
|
||||
@ -38,6 +39,7 @@ class SCEditRoomInfoRes {
|
||||
_roomAccount = roomAccount;
|
||||
_userId = userId;
|
||||
_roomCover = roomCover;
|
||||
_roomBackground = roomBackground;
|
||||
_roomName = roomName;
|
||||
_roomDesc = roomDesc;
|
||||
_event = event;
|
||||
@ -57,6 +59,7 @@ class SCEditRoomInfoRes {
|
||||
_roomAccount = json['roomAccount'];
|
||||
_userId = json['userId'];
|
||||
_roomCover = json['roomCover'] ?? json['cover'];
|
||||
_roomBackground = json['roomBackground'];
|
||||
_roomName = json['roomName'];
|
||||
_roomDesc = json['roomDesc'];
|
||||
_event = json['event'];
|
||||
@ -74,6 +77,7 @@ class SCEditRoomInfoRes {
|
||||
String? _roomAccount;
|
||||
String? _userId;
|
||||
String? _roomCover;
|
||||
String? _roomBackground;
|
||||
String? _roomName;
|
||||
String? _roomDesc;
|
||||
String? _event;
|
||||
@ -91,6 +95,7 @@ class SCEditRoomInfoRes {
|
||||
String? roomAccount,
|
||||
String? userId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
String? event,
|
||||
@ -108,6 +113,7 @@ class SCEditRoomInfoRes {
|
||||
roomAccount: roomAccount ?? _roomAccount,
|
||||
userId: userId ?? _userId,
|
||||
roomCover: roomCover ?? _roomCover,
|
||||
roomBackground: roomBackground ?? _roomBackground,
|
||||
roomName: roomName ?? _roomName,
|
||||
roomDesc: roomDesc ?? _roomDesc,
|
||||
event: event ?? _event,
|
||||
@ -125,6 +131,7 @@ class SCEditRoomInfoRes {
|
||||
String? get roomAccount => _roomAccount;
|
||||
String? get userId => _userId;
|
||||
String? get roomCover => _roomCover;
|
||||
String? get roomBackground => _roomBackground;
|
||||
String? get roomName => _roomName;
|
||||
String? get roomDesc => _roomDesc;
|
||||
String? get event => _event;
|
||||
@ -144,6 +151,7 @@ class SCEditRoomInfoRes {
|
||||
map['roomAccount'] = _roomAccount;
|
||||
map['userId'] = _userId;
|
||||
map['roomCover'] = _roomCover;
|
||||
map['roomBackground'] = _roomBackground;
|
||||
map['roomName'] = _roomName;
|
||||
map['roomDesc'] = _roomDesc;
|
||||
map['event'] = _event;
|
||||
|
||||
@ -90,6 +90,12 @@ abstract class SocialChatUserRepository {
|
||||
String event,
|
||||
);
|
||||
|
||||
///修改房间背景图
|
||||
Future<SCEditRoomInfoRes> updateRoomBackground(
|
||||
String roomId,
|
||||
String roomBackground,
|
||||
);
|
||||
|
||||
///获取指定用户信息
|
||||
Future<SocialChatUserProfile> loadUserInfo(String userId);
|
||||
|
||||
|
||||
@ -76,6 +76,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
specificRoom.roomCover,
|
||||
cachedRoom.roomCover,
|
||||
),
|
||||
roomBackground: SCRoomProfileCache.preferNonEmpty(
|
||||
specificRoom.roomBackground,
|
||||
cachedRoom.roomBackground,
|
||||
),
|
||||
roomName: SCRoomProfileCache.preferNonEmpty(
|
||||
specificRoom.roomName,
|
||||
cachedRoom.roomName,
|
||||
@ -137,11 +141,12 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
fromJson: (json) => MyRoomRes.fromJson(json),
|
||||
);
|
||||
debugPrint(
|
||||
"[Room Cover][specific] roomId=${result.id ?? roomId} roomCover=${result.roomCover ?? ""} roomName=${result.roomName ?? ""}",
|
||||
"[Room Cover][specific] roomId=${result.id ?? roomId} roomCover=${result.roomCover ?? ""} roomBackground=${result.roomBackground ?? ""} roomName=${result.roomName ?? ""}",
|
||||
);
|
||||
await SCRoomProfileCache.saveRoomProfile(
|
||||
roomId: result.id ?? roomId,
|
||||
roomCover: result.roomCover,
|
||||
roomBackground: result.roomBackground,
|
||||
roomName: result.roomName,
|
||||
roomDesc: result.roomDesc,
|
||||
);
|
||||
|
||||
@ -33,6 +33,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart';
|
||||
import 'package:yumi/shared/business_logic/repositories/user_repository.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||
import 'package:yumi/shared/tools/sc_mobile_login_context.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_profile_cache.dart';
|
||||
|
||||
class SCAccountRepository implements SocialChatUserRepository {
|
||||
@ -81,9 +82,11 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
///auth/account/login
|
||||
@override
|
||||
Future<SocialChatLoginRes> loginForAccount(String account, String pwd) async {
|
||||
final data = <String, dynamic>{"account": account, "pwd": pwd};
|
||||
data.addAll(await SCMobileLoginContext.loginBodyFields());
|
||||
final result = await http.post<SocialChatLoginRes>(
|
||||
"e64f27b9ba6b37881120f4584a5444a5c684d8491b703d0af953e5cc6a5f4cec",
|
||||
data: {"account": account, "pwd": pwd},
|
||||
data: data,
|
||||
fromJson: (json) => SocialChatLoginRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
@ -95,9 +98,11 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
String authType,
|
||||
String openId,
|
||||
) async {
|
||||
final data = <String, dynamic>{"authType": authType, "openId": openId};
|
||||
data.addAll(await SCMobileLoginContext.loginBodyFields());
|
||||
final result = await http.post<SocialChatLoginRes>(
|
||||
"e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44",
|
||||
data: {"authType": authType, "openId": openId},
|
||||
data: data,
|
||||
fromJson: (json) => SocialChatLoginRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
@ -275,6 +280,34 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
///room/profile/background
|
||||
@override
|
||||
Future<SCEditRoomInfoRes> updateRoomBackground(
|
||||
String roomId,
|
||||
String roomBackground,
|
||||
) async {
|
||||
final payload = {"roomId": roomId, "roomBackground": roomBackground};
|
||||
debugPrint("[Room Background Save] request payload=$payload");
|
||||
final result = await http.put(
|
||||
"/room/profile/background",
|
||||
data: payload,
|
||||
fromJson: (json) => SCEditRoomInfoRes.fromJson(json),
|
||||
);
|
||||
final resolvedBackground =
|
||||
(result.roomBackground ?? "").trim().isNotEmpty
|
||||
? result.roomBackground
|
||||
: roomBackground;
|
||||
debugPrint("[Room Background Save] response=${result.toJson()}");
|
||||
await SCRoomProfileCache.saveRoomProfile(
|
||||
roomId: result.id ?? roomId,
|
||||
roomCover: result.roomCover,
|
||||
roomBackground: resolvedBackground,
|
||||
roomName: result.roomName,
|
||||
roomDesc: result.roomDesc,
|
||||
);
|
||||
return result.copyWith(roomBackground: resolvedBackground);
|
||||
}
|
||||
|
||||
///user/user-profile
|
||||
@override
|
||||
Future<SocialChatUserProfile> loadUserInfo(String userId) async {
|
||||
|
||||
@ -61,7 +61,12 @@ class SCGiftVapSvgaManager {
|
||||
|
||||
void setMute(bool muteMusic) {
|
||||
_mute = muteMusic;
|
||||
DataPersistence.setPlayGiftMusic(_mute);
|
||||
_rsc?.muted = _mute;
|
||||
final vapController = _rgc;
|
||||
if (vapController != null) {
|
||||
unawaited(vapController.setMute(_mute));
|
||||
}
|
||||
unawaited(DataPersistence.setPlayGiftMusic(_mute));
|
||||
}
|
||||
|
||||
bool getMute() {
|
||||
@ -415,6 +420,7 @@ class SCGiftVapSvgaManager {
|
||||
_mute = DataPersistence.getPlayGiftMusic();
|
||||
_dis = false;
|
||||
_rgc = vapController;
|
||||
unawaited(vapController.setMute(_mute));
|
||||
_log(
|
||||
'bindVapCtrl hasVapCtrl=${_rgc != null} '
|
||||
'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute',
|
||||
@ -439,8 +445,10 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
void bindSvgaCtrl(SVGAAnimationController svgaController) {
|
||||
_mute = DataPersistence.getPlayGiftMusic();
|
||||
_dis = false;
|
||||
_rsc = svgaController;
|
||||
_rsc?.muted = _mute;
|
||||
_log(
|
||||
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
|
||||
'hasVapCtrl=${_rgc != null} queue=$_queueSummary',
|
||||
@ -565,6 +573,7 @@ class SCGiftVapSvgaManager {
|
||||
return;
|
||||
}
|
||||
_log('use prepared asset svga path=${task.path}');
|
||||
_rsc?.muted = _mute;
|
||||
_rsc?.videoItem = entity;
|
||||
_rsc?.reset();
|
||||
_rsc?.forward();
|
||||
@ -591,6 +600,7 @@ class SCGiftVapSvgaManager {
|
||||
return;
|
||||
}
|
||||
_log('play local svga file path=${task.path}');
|
||||
_rsc?.muted = _mute;
|
||||
_rsc?.videoItem = entity;
|
||||
_rsc?.reset();
|
||||
_rsc?.forward();
|
||||
@ -634,6 +644,7 @@ class SCGiftVapSvgaManager {
|
||||
return;
|
||||
}
|
||||
_log('use prepared network svga path=${task.path}');
|
||||
_rsc?.muted = _mute;
|
||||
_rsc?.videoItem = entity;
|
||||
_rsc?.reset();
|
||||
_rsc?.forward();
|
||||
|
||||
62
lib/shared/tools/sc_lucky_gift_win_sound_player.dart
Normal file
@ -0,0 +1,62 @@
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
|
||||
class SCLuckyGiftWinSoundPlayer {
|
||||
static const String assetPath =
|
||||
"sc_images/room/anim/luck_gift/lucky_gift_win.mp3";
|
||||
static const Duration _sameEventDedupeWindow = Duration(seconds: 3);
|
||||
|
||||
static final AudioPlayer _player = AudioPlayer();
|
||||
static String? _lastEventKey;
|
||||
static DateTime? _lastPlayedAt;
|
||||
|
||||
static String buildEventKey({
|
||||
String? giftId,
|
||||
String? userId,
|
||||
String? toUserId,
|
||||
num? awardAmount,
|
||||
}) {
|
||||
return [
|
||||
(giftId ?? '').trim(),
|
||||
(userId ?? '').trim(),
|
||||
(toUserId ?? '').trim(),
|
||||
_formatNum(awardAmount),
|
||||
].join('|');
|
||||
}
|
||||
|
||||
static Future<void> play({String? eventKey}) async {
|
||||
if (DataPersistence.getPlayGiftMusic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final normalizedEventKey = (eventKey ?? '').trim();
|
||||
final now = DateTime.now();
|
||||
final lastPlayedAt = _lastPlayedAt;
|
||||
if (normalizedEventKey.isNotEmpty &&
|
||||
normalizedEventKey == _lastEventKey &&
|
||||
lastPlayedAt != null &&
|
||||
now.difference(lastPlayedAt) < _sameEventDedupeWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastEventKey = normalizedEventKey.isEmpty ? null : normalizedEventKey;
|
||||
_lastPlayedAt = now;
|
||||
try {
|
||||
await _player.stop();
|
||||
await _player.play(AssetSource(assetPath));
|
||||
} catch (error) {
|
||||
debugPrint('播放幸运礼物中奖音效失败: $error');
|
||||
}
|
||||
}
|
||||
|
||||
static String _formatNum(num? value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
if (value % 1 == 0) {
|
||||
return value.toInt().toString();
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
61
lib/shared/tools/sc_mobile_login_context.dart
Normal file
@ -0,0 +1,61 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class SCMobileLoginContext {
|
||||
static const MethodChannel _channel = MethodChannel(
|
||||
'com.org.yumiparty/mobile_context',
|
||||
);
|
||||
|
||||
static Future<Map<String, dynamic>> loginBodyFields() async {
|
||||
return <String, dynamic>{
|
||||
'mobileLang': mobileLang,
|
||||
'mobileZone': await mobileZone(),
|
||||
};
|
||||
}
|
||||
|
||||
static String get mobileLang {
|
||||
final locales = ui.PlatformDispatcher.instance.locales;
|
||||
final locale =
|
||||
locales.isNotEmpty
|
||||
? locales.first
|
||||
: ui.PlatformDispatcher.instance.locale;
|
||||
final languageTag = locale.toLanguageTag().trim();
|
||||
if (languageTag.isNotEmpty) {
|
||||
return languageTag;
|
||||
}
|
||||
return locale.languageCode.trim();
|
||||
}
|
||||
|
||||
static Future<String> mobileZone() async {
|
||||
try {
|
||||
final zone = await _channel.invokeMethod<String>('getTimeZoneIdentifier');
|
||||
final value = zone?.trim() ?? '';
|
||||
if (value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('get mobile timezone failed: ${e.code} ${e.message}');
|
||||
} catch (e) {
|
||||
debugPrint('get mobile timezone failed: $e');
|
||||
}
|
||||
|
||||
return _fallbackZone();
|
||||
}
|
||||
|
||||
static String _fallbackZone() {
|
||||
final now = DateTime.now();
|
||||
final zoneName = now.timeZoneName.trim();
|
||||
if (zoneName.contains('/')) {
|
||||
return zoneName;
|
||||
}
|
||||
|
||||
final offset = now.timeZoneOffset;
|
||||
final sign = offset.isNegative ? '-' : '+';
|
||||
final absoluteOffset = offset.abs();
|
||||
final hours = absoluteOffset.inHours.toString().padLeft(2, '0');
|
||||
final minutes = (absoluteOffset.inMinutes % 60).toString().padLeft(2, '0');
|
||||
return 'UTC$sign$hours:$minutes';
|
||||
}
|
||||
}
|
||||
@ -57,6 +57,7 @@ class SCRoomProfileCache {
|
||||
static Future<void> saveRoomProfile({
|
||||
required String roomId,
|
||||
String? roomCover,
|
||||
String? roomBackground,
|
||||
String? roomName,
|
||||
String? roomDesc,
|
||||
}) async {
|
||||
@ -66,6 +67,8 @@ class SCRoomProfileCache {
|
||||
final current = getRoomProfile(roomId);
|
||||
final payload = <String, String>{
|
||||
"roomCover": preferNonEmpty(roomCover, current["roomCover"]) ?? "",
|
||||
"roomBackground":
|
||||
preferNonEmpty(roomBackground, current["roomBackground"]) ?? "",
|
||||
"roomName": preferNonEmpty(roomName, current["roomName"]) ?? "",
|
||||
"roomDesc": preferNonEmpty(roomDesc, current["roomDesc"]) ?? "",
|
||||
};
|
||||
@ -79,6 +82,10 @@ class SCRoomProfileCache {
|
||||
}
|
||||
return room.copyWith(
|
||||
roomCover: preferCachedValue(cache["roomCover"], room.roomCover),
|
||||
roomBackground: preferCachedValue(
|
||||
cache["roomBackground"],
|
||||
room.roomBackground,
|
||||
),
|
||||
roomName: preferCachedValue(cache["roomName"], room.roomName),
|
||||
roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc),
|
||||
);
|
||||
@ -96,6 +103,10 @@ class SCRoomProfileCache {
|
||||
return room.copyWith(
|
||||
roomProfile: roomProfile.copyWith(
|
||||
roomCover: preferCachedValue(cache["roomCover"], roomProfile.roomCover),
|
||||
roomBackground: preferCachedValue(
|
||||
cache["roomBackground"],
|
||||
roomProfile.roomBackground,
|
||||
),
|
||||
roomName: preferCachedValue(cache["roomName"], roomProfile.roomName),
|
||||
roomDesc: preferCachedValue(cache["roomDesc"], roomProfile.roomDesc),
|
||||
),
|
||||
@ -109,6 +120,10 @@ class SCRoomProfileCache {
|
||||
}
|
||||
return room.copyWith(
|
||||
roomCover: preferCachedValue(cache["roomCover"], room.roomCover),
|
||||
roomBackground: preferCachedValue(
|
||||
cache["roomBackground"],
|
||||
room.roomBackground,
|
||||
),
|
||||
roomName: preferCachedValue(cache["roomName"], room.roomName),
|
||||
roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc),
|
||||
);
|
||||
@ -131,6 +146,10 @@ class SCRoomProfileCache {
|
||||
cache["roomCover"],
|
||||
basicRoomProfile.roomCover,
|
||||
),
|
||||
roomBackground: preferCachedValue(
|
||||
cache["roomBackground"],
|
||||
basicRoomProfile.roomBackground,
|
||||
),
|
||||
roomName: preferCachedValue(
|
||||
cache["roomName"],
|
||||
basicRoomProfile.roomName,
|
||||
|
||||
@ -21,16 +21,34 @@ class LGiftAnimalPage extends StatefulWidget {
|
||||
|
||||
class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
with TickerProviderStateMixin {
|
||||
static const double _tickerHeight = 52;
|
||||
static const double _slotExtent = 70;
|
||||
static const double _entryOffset = 60;
|
||||
|
||||
late final GiftAnimationManager _giftAnimationManager;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableHeight =
|
||||
constraints.hasBoundedHeight ? constraints.maxHeight : 332.w;
|
||||
final visibleSlotCount = _resolveVisibleSlotCount(availableHeight);
|
||||
_syncVisibleSlotCount(visibleSlotCount);
|
||||
if (visibleSlotCount <= 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 332.w,
|
||||
height: _requiredHeightForSlots(visibleSlotCount),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: List.generate(4, (index) => _GiftTickerSlot(index: index)),
|
||||
children: List.generate(
|
||||
visibleSlotCount,
|
||||
(index) => _GiftTickerSlot(index: index),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -52,8 +70,8 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
|
||||
void initAnimal() {
|
||||
List<LGiftScrollingScreenAnimsBean> beans = [];
|
||||
double top = 60;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int i = 0; i < GiftAnimationManager.maxVisibleSlots; i++) {
|
||||
final targetTop = i * _slotExtent;
|
||||
var bean = LGiftScrollingScreenAnimsBean();
|
||||
var controller = AnimationController(
|
||||
value: 0,
|
||||
@ -66,10 +84,10 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
duration: const Duration(milliseconds: 280),
|
||||
vsync: this,
|
||||
);
|
||||
bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top);
|
||||
bean.luckyGiftPinnedMargin = EdgeInsets.only(top: targetTop.w);
|
||||
bean.verticalAnimation = EdgeInsetsTween(
|
||||
begin: EdgeInsets.only(top: top),
|
||||
end: EdgeInsets.only(top: 0),
|
||||
begin: EdgeInsets.only(top: (targetTop + _entryOffset).w),
|
||||
end: EdgeInsets.only(top: targetTop.w),
|
||||
).animate(
|
||||
CurvedAnimation(parent: controller, curve: Curves.easeOutCubic),
|
||||
);
|
||||
@ -95,11 +113,36 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
),
|
||||
);
|
||||
beans.add(bean);
|
||||
top = top + 70;
|
||||
}
|
||||
_giftAnimationManager.attachAnimationControllers(beans);
|
||||
}
|
||||
|
||||
int _resolveVisibleSlotCount(double availableHeight) {
|
||||
if (!availableHeight.isFinite || availableHeight <= 0) {
|
||||
return GiftAnimationManager.maxVisibleSlots;
|
||||
}
|
||||
final tickerHeight = _tickerHeight.w;
|
||||
if (availableHeight < tickerHeight) {
|
||||
return 0;
|
||||
}
|
||||
final slotExtent = _slotExtent.w;
|
||||
final count = ((availableHeight - tickerHeight) / slotExtent).floor() + 1;
|
||||
return count.clamp(1, GiftAnimationManager.maxVisibleSlots).toInt();
|
||||
}
|
||||
|
||||
double _requiredHeightForSlots(int slotCount) {
|
||||
return _tickerHeight.w + ((slotCount - 1) * _slotExtent.w);
|
||||
}
|
||||
|
||||
void _syncVisibleSlotCount(int visibleSlotCount) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_giftAnimationManager.setVisibleSlotCount(visibleSlotCount);
|
||||
});
|
||||
}
|
||||
|
||||
String getBg(String type) {
|
||||
return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png";
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_lucky_gift_win_sound_player.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
@ -83,6 +84,17 @@ class _FloatingLuckGiftScreenWidgetState
|
||||
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
||||
).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn));
|
||||
|
||||
unawaited(
|
||||
SCLuckyGiftWinSoundPlayer.play(
|
||||
eventKey: SCLuckyGiftWinSoundPlayer.buildEventKey(
|
||||
giftId: widget.message.giftId,
|
||||
userId: widget.message.userId,
|
||||
toUserId: widget.message.toUserId,
|
||||
awardAmount: widget.message.coins,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 监听主动画完成
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed && !_isSwipeAnimating) {
|
||||
|
||||
@ -24,6 +24,8 @@ import '../../components/sc_debounce_widget.dart';
|
||||
class RoomBottomWidget extends StatefulWidget {
|
||||
const RoomBottomWidget({super.key, this.showGiftComboButton = true});
|
||||
|
||||
static const double floatingButtonHostHeight = _floatingButtonHostHeight;
|
||||
|
||||
final bool showGiftComboButton;
|
||||
|
||||
@override
|
||||
|
||||
@ -38,7 +38,7 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
||||
static const int _menuReport = 5;
|
||||
static const int _menuBackground = 6;
|
||||
static const int _menuMusic = 7;
|
||||
static bool get _showBackgroundFeature => false;
|
||||
static bool get _showBackgroundFeature => true;
|
||||
|
||||
List<RoomMenu> items2 = [];
|
||||
|
||||
|
||||
149
local_packages/flutter_svga-0.0.13-patched/CHANGELOG.md
Normal file
@ -0,0 +1,149 @@
|
||||
## [0.0.13] - 2026-01-20
|
||||
|
||||
### 🚀 New Features
|
||||
|
||||
- **Volume Control** (#9, PR #10): Added audio volume control to `SVGAAnimationController`
|
||||
- `controller.volume = 0.5` — Set volume level (0.0 to 1.0)
|
||||
- `controller.muted = true/false` — Toggle mute without losing volume setting
|
||||
- Thanks to [@Sansuihe](https://github.com/Sansuihe) for the contribution!
|
||||
|
||||
### ⚡ Performance Improvements
|
||||
|
||||
- **Memory Optimization** (#8): Reduced Dart heap memory usage by clearing protobuf internal structures after parsing
|
||||
- Added `freeze()` method to `MovieEntity` that clears raw image data after extraction
|
||||
- Addresses high memory from `FieldSet`, `PbList`, and `FrameEntity` instances
|
||||
- Thanks to [@jianleepb](https://github.com/jianleepb) for reporting!
|
||||
|
||||
---
|
||||
|
||||
## [0.0.12] - 2025-12-24
|
||||
|
||||
### 🚀 Improvements
|
||||
|
||||
- **Updated Dart SDK constraint** to `^3.10.4` for compatibility with Flutter 3.38.5
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **Shortened package description** for pub.dev compliance (under 180 characters)
|
||||
- **Fixed lint warnings** in generated protobuf files by adding `no_leading_underscores_for_local_identifiers` to ignore rules
|
||||
|
||||
---
|
||||
|
||||
## [0.0.11] - 2025-12-10
|
||||
|
||||
### 📝 Documentation
|
||||
|
||||
- **Enhanced README with competitive positioning**
|
||||
- Added "Why flutter_svga?" section highlighting active maintenance vs archived `svgaplayer_flutter`
|
||||
- Added comparison table (flutter_svga vs svgaplayer_flutter vs Lottie)
|
||||
- Added migration guide for users switching from `svgaplayer_flutter`
|
||||
|
||||
- **Improved SEO & Discoverability**
|
||||
- Updated `pubspec.yaml` description with better keywords (actively maintained, alternative, replacement)
|
||||
- Added relevant topics: animation, svga, vector-graphics, lottie-alternative, ui-effects
|
||||
|
||||
---
|
||||
|
||||
## [0.0.10] - 2025-12-03
|
||||
|
||||
### 📝 Documentation
|
||||
- Updated README to version 0.0.10 for pub.dev visibility
|
||||
- Added contributors @Sansuihe and @tungpham6195 to README
|
||||
|
||||
---
|
||||
|
||||
## [0.0.9] - 2025-12-03
|
||||
|
||||
### 🛠 Fixes
|
||||
- Fixed audio cache collision bug where different SVGA files with the same `audioKey` but different audio content incorrectly shared cached files (#6).
|
||||
Now uses MD5 hash of audio data to ensure unique cache files.
|
||||
(Thanks to [@Sansuihe](https://github.com/Sansuihe) for reporting and suggesting the fix.)
|
||||
|
||||
### 🚀 Improvements
|
||||
- Protobuf dependency updated to `^6.0.0`, ensuring compatibility with `retrofit_generator >=10.1.0` (#7).
|
||||
(Thanks to [@tungpham6195](https://github.com/tungpham6195) for reporting.)
|
||||
|
||||
---
|
||||
|
||||
## [0.0.8] - 2025-10-18
|
||||
|
||||
|
||||
### 🛠 Fixes
|
||||
- Fixed repeated music playback bug when reusing the same audio key.
|
||||
(Thanks to [@wonderkidshihab](https://github.com/wonderkidshihab) for the PR and [@Sansuihe](https://github.com/Sansuihe) for reporting.)
|
||||
|
||||
### 🚀 Improvements
|
||||
- Optimized audio handling and cache logic for better playback stability.
|
||||
- Updated README and credits for contributors.
|
||||
- Version bump for pub.dev publication.
|
||||
|
||||
---
|
||||
|
||||
## [0.0.7] - 2025-10-18
|
||||
|
||||
### Fixed
|
||||
- 🎵 Fixed repeated music playback bug when same audio keys are used (#3)
|
||||
- 🛡️ Improved race condition handling in audio playback logic
|
||||
- ✅ Added early return check to prevent duplicate audio playback
|
||||
|
||||
### Maintenance
|
||||
- Enhanced audio layer synchronization and error handling
|
||||
|
||||
---
|
||||
|
||||
|
||||
## [0.0.6] - 2025-06-26
|
||||
|
||||
### Fixed
|
||||
- 🎧 Prevent crash when AudioPlayer is accessed after dispose (#1)
|
||||
- 🛠️ Updated to support protobuf 4+, other latest dependencies and use Flutter 3.32.5 (#2)
|
||||
|
||||
### Maintenance
|
||||
- Improved safety checks in SVGAAudioLayer
|
||||
- Compatible with Dart 3.0 and Flutter 3.32+
|
||||
|
||||
---
|
||||
|
||||
## 0.0.5 - Update (2025-03-14)
|
||||
|
||||
### 🔥 Upgrade to flutter version 3.29.2
|
||||
|
||||
---
|
||||
|
||||
## 0.0.4 - Update (2025-02-09)
|
||||
|
||||
### 🔥 New Features & Improvements
|
||||
- ✅ **Added Audio Support**: Integrated audio playback within SVGA animations using the `audioplayers` package.
|
||||
|
||||
---
|
||||
|
||||
## 0.0.3 - Update (2025-01-30)
|
||||
|
||||
### 🔥 New Features & Improvements
|
||||
- ✅ **Added Pause & Resume Playback Functions** for SVGA animations.
|
||||
- ✅ **Enhanced Error Handling & Logging** for better debugging.
|
||||
- ✅ **Improved Performance** when loading SVGA animations.
|
||||
|
||||
### 🛠 Fixes & Optimizations
|
||||
- 🛠 Fixed potential crashes when loading SVGA assets.
|
||||
- 🛠 Optimized memory usage for large SVGA files.
|
||||
- 🛠 Improved logging messages for debugging.
|
||||
|
||||
---
|
||||
|
||||
## 0.0.2 - Update (2025-01-30)
|
||||
|
||||
### 🔥 Updates & Improvements
|
||||
- ✅ Added **example GIFs** for better demonstration.
|
||||
- ✅ **Supported all platforms** including **Web & Desktop**.
|
||||
|
||||
---
|
||||
|
||||
## 0.0.1 - Initial Release (2025-01-29)
|
||||
|
||||
### 🎉 New Features
|
||||
- ✅ Added support for **SVGA parsing and rendering**.
|
||||
- ✅ Load **SVGA animations** from **assets** and **network URLs**.
|
||||
- ✅ Implemented **SVGAAnimationController** for playback control.
|
||||
- ✅ Dynamic entity support (text & images).
|
||||
- ✅ Optimized performance for **smooth animations**.
|
||||
26
local_packages/flutter_svga-0.0.13-patched/LICENSE
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
---
|
||||
|
||||
### **📄 `LICENSE`** (MIT License)
|
||||
```plaintext
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 5alafawyyy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
353
local_packages/flutter_svga-0.0.13-patched/README.md
Normal file
@ -0,0 +1,353 @@
|
||||
# flutter_svga
|
||||
|
||||
A **Flutter package** for parsing and rendering **SVGA animations** efficiently.
|
||||
SVGA is a lightweight and powerful animation format used for **dynamic UI effects** in mobile applications.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/5alafawyyy/flutter_svga/master/example.gif" width="300"/>
|
||||
<img src="https://raw.githubusercontent.com/5alafawyyy/flutter_svga/master/example1.gif" width="300"/>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **Why flutter_svga?**
|
||||
|
||||
### ✅ **Actively Maintained**
|
||||
Unlike **`svgaplayer_flutter`** (archived since Feb 2023), `flutter_svga` is **actively maintained** with regular updates, bug fixes, and community support.
|
||||
|
||||
### ⚡ **High Performance**
|
||||
- **Intelligent caching system** reduces network usage and load times
|
||||
- **Binary format** smaller than JSON-based Lottie files
|
||||
- **Optimized rendering** for smooth 60 FPS animations
|
||||
|
||||
### 🎨 **Feature-Rich**
|
||||
- **Audio playback** integrated directly in animations
|
||||
- **Dynamic elements** (replace text, images, colors at runtime)
|
||||
- **Full platform support** (Android, iOS, Web, macOS, Linux, Windows)
|
||||
|
||||
### 📦 **Comparison**
|
||||
|
||||
| Feature | flutter_svga | svgaplayer_flutter | Lottie |
|
||||
|---------|--------------|-------------------|--------|
|
||||
| **Status** | ✅ **Active** | ❌ Archived (Feb 2023) | ✅ Active |
|
||||
| **Caching** | ✅ Built-in intelligent cache | ❌ No | ⚠️ Manual |
|
||||
| **Audio Support** | ✅ Integrated | ❌ No | ✅ Yes |
|
||||
| **File Size** | 🟢 Small (binary) | 🟢 Small (binary) | 🟡 Larger (JSON) |
|
||||
| **Dynamic Elements** | ✅ Text, Images, Drawers | ⚠️ Limited | ✅ Yes |
|
||||
| **Platform Support** | ✅ All 6 platforms | ⚠️ Mobile only | ✅ All platforms |
|
||||
| **Performance** | ⚡ Optimized | ⚡ Good | ⚡ Good |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 **Migrating from svgaplayer_flutter**
|
||||
|
||||
Switching from the archived `svgaplayer_flutter` is simple:
|
||||
|
||||
### 1. Update Dependencies
|
||||
```yaml
|
||||
dependencies:
|
||||
# svgaplayer_flutter: ^2.2.0 # Remove old package
|
||||
flutter_svga: ^0.0.13 # Add new package
|
||||
```
|
||||
|
||||
### 2. Update Imports
|
||||
```dart
|
||||
// Old
|
||||
// import 'package:svgaplayer_flutter/svgaplayer_flutter.dart';
|
||||
|
||||
// New
|
||||
import 'package:flutter_svga/flutter_svga.dart';
|
||||
```
|
||||
|
||||
### 3. API Usage (mostly compatible)
|
||||
The API is similar, with some enhancements:
|
||||
|
||||
```dart
|
||||
// Both packages use similar controller patterns
|
||||
SVGAAnimationController controller = SVGAAnimationController(vsync: this);
|
||||
|
||||
// Loading remains the same
|
||||
final videoItem = await SVGAParser.shared.decodeFromAssets("assets/animation.svga");
|
||||
controller.videoItem = videoItem;
|
||||
controller.repeat();
|
||||
```
|
||||
|
||||
**🎉 Bonus**: You now get **automatic caching**, **audio support**, and **better performance** with zero code changes!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **Features**
|
||||
|
||||
✔️ Parse and render **SVGA animations** in Flutter.
|
||||
✔️ Load SVGA files from **assets** and **network URLs**.
|
||||
✔️ **Intelligent caching system** for faster loading and reduced network usage.
|
||||
✔️ Supports **custom dynamic elements** (text, images, animations).
|
||||
✔️ **Optimized playback performance** with animation controllers.
|
||||
✔️ **Integrated audio playback** within SVGA animations.
|
||||
✔️ Works on **Android & iOS** (Web & Desktop support coming soon).
|
||||
✔️ Easy **loop, stop, and seek** functions.
|
||||
|
||||
---
|
||||
|
||||
## 📌 **Installation**
|
||||
|
||||
Add **flutter_svga** to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter_svga: ^0.0.13
|
||||
|
||||
|
||||
```
|
||||
Then, install dependencies:
|
||||
|
||||
```sh
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎬 **Basic Usage**
|
||||
|
||||
### ✅ **Playing an SVGA Animation from Assets**
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svga/flutter_svga.dart';
|
||||
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: Text("Flutter SVGA Example")),
|
||||
body: Center(
|
||||
child: SVGAEasyPlayer(
|
||||
assetsName: "assets/sample_with_audio.svga",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌍 **Playing SVGA from a Network URL**
|
||||
```dart
|
||||
SVGAEasyPlayer(
|
||||
resUrl: "https://example.com/sample.svga",
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎭 **Advanced Usage: Using SVGAAnimationController**
|
||||
|
||||
### ✅ **Controlling Animation Playback**
|
||||
```dart
|
||||
class MySVGAWidget extends StatefulWidget {
|
||||
@override
|
||||
_MySVGAWidgetState createState() => _MySVGAWidgetState();
|
||||
}
|
||||
|
||||
class _MySVGAWidgetState extends State<MySVGAWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late SVGAAnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = SVGAAnimationController(vsync: this);
|
||||
SVGAParser.shared.decodeFromAssets("assets/sample.svga").then((video) {
|
||||
_controller.videoItem = video;
|
||||
_controller.repeat();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SVGAImage(_controller);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 **Customization & Dynamic Elements**
|
||||
|
||||
### ✅ **Adding Dynamic Text**
|
||||
```dart
|
||||
controller.videoItem!.dynamicItem.setText(
|
||||
TextPainter(
|
||||
text: TextSpan(
|
||||
text: "Hello SVGA!",
|
||||
style: TextStyle(color: Colors.red, fontSize: 18),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
"text_layer",
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ **Replacing an Image Dynamically**
|
||||
```dart
|
||||
controller.videoItem!.dynamicItem.setImageWithUrl(
|
||||
"https://example.com/new_image.png",
|
||||
"image_layer",
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ **Hiding a Layer**
|
||||
```dart
|
||||
controller.videoItem!.dynamicItem.setHidden(true, "layer_to_hide");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ **Caching (New!)**
|
||||
|
||||
**Automatic performance optimization with zero breaking changes:**
|
||||
|
||||
```dart
|
||||
// Caching works automatically - no code changes needed!
|
||||
final animation = await SVGAParser.shared.decodeFromURL(
|
||||
"https://example.com/animation.svga"
|
||||
);
|
||||
|
||||
// Optional: Configure cache settings
|
||||
SVGACache.shared.setMaxCacheSize(50 * 1024 * 1024); // 50MB
|
||||
SVGACache.shared.setMaxAge(const Duration(days: 3)); // 3 days
|
||||
|
||||
// Optional: Manage cache
|
||||
await SVGACache.shared.clear(); // Clear all cache
|
||||
final stats = await SVGACache.shared.getStats(); // Get cache info
|
||||
```
|
||||
|
||||
**📋 See [CACHE.md](CACHE.md) for complete caching documentation and examples.**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **Playback Controls**
|
||||
```dart
|
||||
controller.forward(); // Play once
|
||||
controller.repeat(); // Loop playback
|
||||
controller.stop(); // Stop animation
|
||||
controller.value = 0; // Reset to first frame
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔊 **Audio Volume Control**
|
||||
```dart
|
||||
// Set volume (0.0 to 1.0)
|
||||
controller.volume = 0.5;
|
||||
|
||||
// Mute audio (preserves volume setting)
|
||||
controller.muted = true;
|
||||
|
||||
// Unmute audio
|
||||
controller.muted = false;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠 **Common Issues & Solutions**
|
||||
|
||||
### ❌ **Black Screen when Loading SVGA**
|
||||
✅ **Solution:** Ensure your `svga` files are correctly placed inside `assets/` and registered in `pubspec.yaml`.
|
||||
```yaml
|
||||
flutter:
|
||||
assets:
|
||||
- assets/sample.svga
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ **SVGA Not Loading from Network**
|
||||
✅ **Solution:** Ensure the SVGA file is accessible via HTTPS. Test the URL in a browser.
|
||||
```dart
|
||||
SVGAEasyPlayer(
|
||||
resUrl: "https://example.com/sample.svga",
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ **Animation Freezes or Doesn't Play**
|
||||
✅ **Solution:** Use `setState` after loading SVGA to rebuild the widget.
|
||||
```dart
|
||||
setState(() {
|
||||
_controller.videoItem = video;
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 **Supported Platforms**
|
||||
|
||||
| Platform | Supported | Audio Support |
|
||||
|----------|-----------|---------------|
|
||||
| ✅ Android | ✔️ Yes | ✔️ Yes |
|
||||
| ✅ iOS | ✔️ Yes | ✔️ Yes |
|
||||
| ✅ Linux | ✔️ Yes | ✔️ Yes |
|
||||
| ✅ Web | ✔️ Yes | ❌ No |
|
||||
| ✅ macOS | ✔️ Yes | ✔️ Yes |
|
||||
| ✅ Desktop | ✔️ Yes | ✔️ Yes |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 **Changelog**
|
||||
See the latest changes in [`CHANGELOG.md`](CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
## 📜 **License**
|
||||
This package is licensed under the **MIT License**. See [`LICENSE`](LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 **Contributing**
|
||||
- If you find a **bug**, report it [here](https://github.com/5alafawyyy/flutter_svga/issues).
|
||||
- Pull requests are welcome! See [`CONTRIBUTING.md`](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 **Authors & Contributors**
|
||||
|
||||
### 🏗 **Core Author**
|
||||
- **[5alafawyyy](https://github.com/5alafawyyy)** — Lead Developer, Maintainer, and Flutter Integration Engineer.
|
||||
|
||||
|
||||
### 🤝 **Contributors**
|
||||
Special thanks to the amazing contributors who improved **flutter_svga**:
|
||||
|
||||
| Contributor | Contribution | GitHub |
|
||||
|--------------|--------------|--------|
|
||||
| **[wonderkidshihab](https://github.com/wonderkidshihab)** | Fixed repeated music playback bug (#3) | 🧩 |
|
||||
| **[Sansuihe](https://github.com/Sansuihe)** | Identified and proposed MD5-based fix for audio cache collision (#6) | 🎵 |
|
||||
| **[tungpham6195](https://github.com/tungpham6195)** | Reported protobuf dependency compatibility issue (#7) | 📦 |
|
||||
|
||||
> Want to contribute? Read [CONTRIBUTING.md](CONTRIBUTING.md) and submit your PR — we’d love your help!
|
||||
|
||||
---
|
||||
|
||||
🚀 **Enjoy using SVGA animations in your Flutter app!** 🚀
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
# Exclude generated protobuf files from analysis
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/src/proto/*.pb.dart
|
||||
- lib/src/proto/*.pbenum.dart
|
||||
- lib/src/proto/*.pbjson.dart
|
||||
- lib/src/proto/*.pbserver.dart
|
||||
@ -0,0 +1,6 @@
|
||||
export 'src/cache.dart';
|
||||
export 'src/dynamic_entity.dart';
|
||||
export 'src/parser.dart';
|
||||
export 'src/player.dart';
|
||||
export 'src/proto/svga.pb.dart'
|
||||
show MovieEntity, MovieParams, ShapeEntity, FrameEntity;
|
||||
@ -0,0 +1,115 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'proto/svga.pb.dart';
|
||||
|
||||
class SVGAAudioLayer {
|
||||
final AudioPlayer _player = AudioPlayer();
|
||||
late final AudioEntity audioItem;
|
||||
late final MovieEntity _videoItem;
|
||||
bool _isReady = false;
|
||||
bool _disposed = false;
|
||||
double _volume = 1.0;
|
||||
|
||||
SVGAAudioLayer(this.audioItem, this._videoItem);
|
||||
|
||||
Future<void> playAudio({double? volume}) async {
|
||||
if (_disposed) return;
|
||||
|
||||
// Prevent duplicate playback if already playing or preparing to play
|
||||
if (_isReady || isPlaying()) return;
|
||||
|
||||
final audioData = _videoItem.audiosData[audioItem.audioKey];
|
||||
if (audioData != null) {
|
||||
// https://github.com/bluefireteam/audioplayers/issues/1782
|
||||
// If need use Bytes, plz upgrade to audioplayers: ^6.0.0
|
||||
// BytesSource source = BytesSource(audioData);
|
||||
final cacheDir = await getApplicationCacheDirectory();
|
||||
if (_disposed) return;
|
||||
|
||||
// Use MD5 hash to ensure unique cache files even when audioKeys collide
|
||||
// across different SVGA files with different audio content
|
||||
final audioHash = md5.convert(audioData).toString();
|
||||
final cacheFile = File(
|
||||
'${cacheDir.path}/temp_${audioItem.audioKey}_$audioHash.mp3',
|
||||
);
|
||||
|
||||
if (!cacheFile.existsSync()) {
|
||||
await cacheFile.writeAsBytes(audioData);
|
||||
if (_disposed) return;
|
||||
}
|
||||
|
||||
try {
|
||||
_isReady = true;
|
||||
if (_disposed) {
|
||||
_isReady = false;
|
||||
return;
|
||||
}
|
||||
// Apply volume before playing
|
||||
if (volume != null) {
|
||||
_volume = volume.clamp(0.0, 1.0);
|
||||
}
|
||||
await _player.setVolume(_volume);
|
||||
if (_disposed) {
|
||||
_isReady = false;
|
||||
return;
|
||||
}
|
||||
await _player.play(DeviceFileSource(cacheFile.path));
|
||||
_isReady = false;
|
||||
// I noticed that this logic exists in the iOS code of SVGAPlayer
|
||||
// but it seems unnecessary.
|
||||
// _player.seek(Duration(milliseconds: audioItem.startTime.toInt()));
|
||||
} catch (e) {
|
||||
_isReady = false;
|
||||
log('Failed to play audio: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the volume for this audio layer.
|
||||
/// [volume] should be between 0.0 (mute) and 1.0 (max volume).
|
||||
void setVolume(double volume) {
|
||||
if (_disposed) return;
|
||||
_volume = volume.clamp(0.0, 1.0);
|
||||
_player.setVolume(_volume);
|
||||
}
|
||||
|
||||
void pauseAudio() {
|
||||
if (_disposed) return;
|
||||
_player.pause();
|
||||
}
|
||||
|
||||
void resumeAudio() {
|
||||
if (_disposed) return;
|
||||
_player.resume();
|
||||
}
|
||||
|
||||
void stopAudio() {
|
||||
if (_disposed) return;
|
||||
if (isPlaying() || isPaused()) unawaited(_player.stop());
|
||||
}
|
||||
|
||||
bool isPlaying() {
|
||||
if (_disposed) return false;
|
||||
return _player.state == PlayerState.playing;
|
||||
}
|
||||
|
||||
bool isPaused() {
|
||||
if (_disposed) return false;
|
||||
return _player.state == PlayerState.paused;
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
if (_disposed) return;
|
||||
if (isPlaying() || isPaused()) {
|
||||
await _player.stop();
|
||||
}
|
||||
_disposed = true;
|
||||
await _player.dispose();
|
||||
}
|
||||
}
|
||||
261
local_packages/flutter_svga-0.0.13-patched/lib/src/cache.dart
Normal file
@ -0,0 +1,261 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// Cache manager for SVGA files to improve loading performance.
|
||||
/// Provides persistent disk caching for parsed SVGA data.
|
||||
class SVGACache {
|
||||
static SVGACache? _instance;
|
||||
static SVGACache get shared => _instance ??= SVGACache._();
|
||||
|
||||
SVGACache._();
|
||||
|
||||
Directory? _cacheDir;
|
||||
bool _enabled = true;
|
||||
int _maxCacheSize = 100 * 1024 * 1024; // 100MB default
|
||||
Duration _maxAge = const Duration(days: 7); // 7 days default
|
||||
|
||||
/// Whether caching is enabled. Defaults to true.
|
||||
bool get isEnabled => _enabled;
|
||||
|
||||
/// Maximum cache size in bytes. Defaults to 100MB.
|
||||
int get maxCacheSize => _maxCacheSize;
|
||||
|
||||
/// Maximum age for cached files. Defaults to 7 days.
|
||||
Duration get maxAge => _maxAge;
|
||||
|
||||
/// Enable or disable caching.
|
||||
void setEnabled(bool enabled) {
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
/// Set maximum cache size in bytes.
|
||||
void setMaxCacheSize(int sizeInBytes) {
|
||||
_maxCacheSize = sizeInBytes;
|
||||
}
|
||||
|
||||
/// Set maximum age for cached files.
|
||||
void setMaxAge(Duration duration) {
|
||||
_maxAge = duration;
|
||||
}
|
||||
|
||||
/// Initialize cache directory
|
||||
Future<void> _ensureCacheDir() async {
|
||||
if (_cacheDir != null) return;
|
||||
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
_cacheDir = Directory('${tempDir.path}/svga_cache');
|
||||
|
||||
if (!await _cacheDir!.exists()) {
|
||||
await _cacheDir!.create(recursive: true);
|
||||
}
|
||||
} catch (e) {
|
||||
// If cache directory creation fails, disable caching
|
||||
_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate cache key from source identifier
|
||||
String _generateCacheKey(String source) {
|
||||
return md5.convert(utf8.encode(source)).toString();
|
||||
}
|
||||
|
||||
/// Get cache file path for a source
|
||||
Future<File?> _getCacheFile(String source) async {
|
||||
if (!_enabled) return null;
|
||||
|
||||
await _ensureCacheDir();
|
||||
if (_cacheDir == null) return null;
|
||||
|
||||
final key = _generateCacheKey(source);
|
||||
return File('${_cacheDir!.path}/$key.svga');
|
||||
}
|
||||
|
||||
/// Check if cached data exists and is still valid
|
||||
Future<bool> _isCacheValid(File cacheFile) async {
|
||||
if (!await cacheFile.exists()) return false;
|
||||
|
||||
final stat = await cacheFile.stat();
|
||||
final age = DateTime.now().difference(stat.modified);
|
||||
|
||||
return age <= _maxAge;
|
||||
}
|
||||
|
||||
/// Get cached raw bytes if available and valid
|
||||
Future<Uint8List?> getRawBytes(String source) async {
|
||||
if (!_enabled) return null;
|
||||
|
||||
try {
|
||||
final cacheFile = await _getCacheFile(source);
|
||||
if (cacheFile == null) return null;
|
||||
|
||||
if (!await _isCacheValid(cacheFile)) {
|
||||
// Remove expired cache
|
||||
await cacheFile.delete().catchError((_) => cacheFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await cacheFile.readAsBytes();
|
||||
} catch (e) {
|
||||
// If cache read fails, return null to fallback to normal loading
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Store raw SVGA bytes in cache
|
||||
Future<void> putRawBytes(String source, Uint8List bytes) async {
|
||||
if (!_enabled) return;
|
||||
|
||||
try {
|
||||
final cacheFile = await _getCacheFile(source);
|
||||
if (cacheFile == null) return;
|
||||
|
||||
// Write the raw bytes to cache
|
||||
await cacheFile.writeAsBytes(bytes);
|
||||
|
||||
// Clean up cache if needed
|
||||
await _cleanupCache();
|
||||
} catch (e) {
|
||||
// If cache write fails, silently continue without caching
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove specific item from cache
|
||||
Future<void> remove(String source) async {
|
||||
if (!_enabled) return;
|
||||
|
||||
try {
|
||||
final cacheFile = await _getCacheFile(source);
|
||||
if (cacheFile != null && await cacheFile.exists()) {
|
||||
await cacheFile.delete();
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all cached data
|
||||
Future<void> clear() async {
|
||||
if (!_enabled || _cacheDir == null) return;
|
||||
|
||||
try {
|
||||
await _ensureCacheDir();
|
||||
if (_cacheDir != null && await _cacheDir!.exists()) {
|
||||
await _cacheDir!.delete(recursive: true);
|
||||
await _cacheDir!.create(recursive: true);
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current cache size in bytes
|
||||
Future<int> getCacheSize() async {
|
||||
if (!_enabled || _cacheDir == null) return 0;
|
||||
|
||||
try {
|
||||
await _ensureCacheDir();
|
||||
if (_cacheDir == null || !await _cacheDir!.exists()) return 0;
|
||||
|
||||
int totalSize = 0;
|
||||
await for (final entity in _cacheDir!.list()) {
|
||||
if (entity is File) {
|
||||
final stat = await entity.stat();
|
||||
totalSize += stat.size;
|
||||
}
|
||||
}
|
||||
return totalSize;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up expired cache and enforce size limits
|
||||
Future<void> _cleanupCache() async {
|
||||
if (!_enabled || _cacheDir == null) return;
|
||||
|
||||
try {
|
||||
await _ensureCacheDir();
|
||||
if (_cacheDir == null || !await _cacheDir!.exists()) return;
|
||||
|
||||
final files = <File>[];
|
||||
await for (final entity in _cacheDir!.list()) {
|
||||
if (entity is File && entity.path.endsWith('.svga')) {
|
||||
files.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove expired files
|
||||
final now = DateTime.now();
|
||||
for (final file in files.toList()) {
|
||||
final stat = await file.stat();
|
||||
if (now.difference(stat.modified) > _maxAge) {
|
||||
await file.delete().catchError((_) => file);
|
||||
files.remove(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Get file stats for sorting
|
||||
final fileStats = <File, FileStat>{};
|
||||
for (final file in files) {
|
||||
fileStats[file] = await file.stat();
|
||||
}
|
||||
|
||||
// Sort by modification time (oldest first)
|
||||
files.sort((a, b) {
|
||||
final statA = fileStats[a]!;
|
||||
final statB = fileStats[b]!;
|
||||
return statA.modified.compareTo(statB.modified);
|
||||
});
|
||||
|
||||
// Calculate total size
|
||||
int totalSize = 0;
|
||||
for (final file in files) {
|
||||
totalSize += fileStats[file]!.size;
|
||||
}
|
||||
|
||||
// Remove oldest files until under size limit
|
||||
while (totalSize > _maxCacheSize && files.isNotEmpty) {
|
||||
final oldestFile = files.removeAt(0);
|
||||
final stat = fileStats[oldestFile]!;
|
||||
totalSize -= stat.size;
|
||||
await oldestFile.delete().catchError((_) => oldestFile);
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cache statistics
|
||||
Future<Map<String, dynamic>> getStats() async {
|
||||
final size = await getCacheSize();
|
||||
int fileCount = 0;
|
||||
|
||||
if (_enabled && _cacheDir != null) {
|
||||
try {
|
||||
await _ensureCacheDir();
|
||||
if (_cacheDir != null && await _cacheDir!.exists()) {
|
||||
await for (final entity in _cacheDir!.list()) {
|
||||
if (entity is File && entity.path.endsWith('.svga')) {
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'enabled': _enabled,
|
||||
'size': size,
|
||||
'maxSize': _maxCacheSize,
|
||||
'fileCount': fileCount,
|
||||
'maxAge': _maxAge.inDays,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
typedef SVGACustomDrawer = Function(Canvas canvas, int frameIndex);
|
||||
|
||||
class SVGADynamicEntity {
|
||||
final Map<String, bool> dynamicHidden = {};
|
||||
final Map<String, ui.Image> dynamicImages = {};
|
||||
final Map<String, TextPainter> dynamicText = {};
|
||||
final Map<String, SVGACustomDrawer> dynamicDrawer = {};
|
||||
|
||||
void setHidden(bool value, String forKey) {
|
||||
dynamicHidden[forKey] = value;
|
||||
}
|
||||
|
||||
void setImage(ui.Image image, String forKey) {
|
||||
dynamicImages[forKey] = image;
|
||||
}
|
||||
|
||||
Future<void> setImageWithUrl(String url, String forKey) async {
|
||||
dynamicImages[forKey] =
|
||||
await decodeImageFromList((await get(Uri.parse(url))).bodyBytes);
|
||||
}
|
||||
|
||||
void setText(TextPainter textPainter, String forKey) {
|
||||
if (textPainter.textDirection == null) {
|
||||
textPainter.textDirection = TextDirection.ltr;
|
||||
textPainter.layout();
|
||||
}
|
||||
dynamicText[forKey] = textPainter;
|
||||
}
|
||||
|
||||
void setDynamicDrawer(SVGACustomDrawer drawer, String forKey) {
|
||||
dynamicDrawer[forKey] = drawer;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
dynamicHidden.clear();
|
||||
dynamicImages.clear();
|
||||
dynamicText.clear();
|
||||
dynamicDrawer.clear();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
part of 'player.dart';
|
||||
|
||||
class SVGAEasyPlayer extends StatefulWidget {
|
||||
final String? resUrl;
|
||||
final String? assetsName;
|
||||
final BoxFit fit;
|
||||
|
||||
const SVGAEasyPlayer({
|
||||
super.key,
|
||||
this.resUrl,
|
||||
this.assetsName,
|
||||
this.fit = BoxFit.contain,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _SVGAEasyPlayerState();
|
||||
}
|
||||
}
|
||||
|
||||
class _SVGAEasyPlayerState extends State<SVGAEasyPlayer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
SVGAAnimationController? animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
animationController = SVGAAnimationController(vsync: this);
|
||||
_tryDecodeSvga();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SVGAEasyPlayer oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.resUrl != widget.resUrl ||
|
||||
oldWidget.assetsName != widget.assetsName) {
|
||||
_tryDecodeSvga();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (animationController == null) {
|
||||
return Container();
|
||||
}
|
||||
return SVGAImage(
|
||||
animationController!,
|
||||
fit: widget.fit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController?.dispose();
|
||||
animationController = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _tryDecodeSvga() {
|
||||
Future<MovieEntity> decode;
|
||||
if (widget.resUrl != null) {
|
||||
decode = SVGAParser.shared.decodeFromURL(widget.resUrl!);
|
||||
} else if (widget.assetsName != null) {
|
||||
decode = SVGAParser.shared.decodeFromAssets(widget.assetsName!);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
decode.then((videoItem) {
|
||||
if (mounted && animationController != null) {
|
||||
animationController!
|
||||
..videoItem = videoItem
|
||||
..repeat();
|
||||
} else {
|
||||
videoItem.dispose();
|
||||
}
|
||||
}).catchError(
|
||||
(e, stack) {
|
||||
FlutterError.reportError(
|
||||
FlutterErrorDetails(
|
||||
exception: e,
|
||||
stack: stack,
|
||||
library: 'SVGAEasyPlayer',
|
||||
context: ErrorDescription('during _tryDecodeSvga'),
|
||||
informationCollector: () => [
|
||||
if (widget.resUrl != null)
|
||||
StringProperty('resUrl', widget.resUrl),
|
||||
if (widget.assetsName != null)
|
||||
StringProperty('assetsName', widget.assetsName),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
463
local_packages/flutter_svga-0.0.13-patched/lib/src/painter.dart
Normal file
@ -0,0 +1,463 @@
|
||||
part of 'player.dart';
|
||||
|
||||
class _SVGAPainter extends CustomPainter {
|
||||
final BoxFit fit;
|
||||
final SVGAAnimationController controller;
|
||||
int get currentFrame => controller.currentFrame;
|
||||
MovieEntity get videoItem => controller.videoItem!;
|
||||
final FilterQuality filterQuality;
|
||||
|
||||
/// Guaranteed to draw within the canvas bounds
|
||||
final bool clipRect;
|
||||
_SVGAPainter(
|
||||
this.controller, {
|
||||
this.fit = BoxFit.contain,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
this.clipRect = true,
|
||||
}) : assert(
|
||||
controller.videoItem != null, 'Invalid SVGAAnimationController!'),
|
||||
super(repaint: controller);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (controller._canvasNeedsClear) {
|
||||
// mark cleared
|
||||
controller._canvasNeedsClear = false;
|
||||
return;
|
||||
}
|
||||
if (size.isEmpty || controller.videoItem == null) return;
|
||||
final params = videoItem.params;
|
||||
final Size viewBoxSize = Size(params.viewBoxWidth, params.viewBoxHeight);
|
||||
if (viewBoxSize.isEmpty) return;
|
||||
canvas.save();
|
||||
try {
|
||||
final canvasRect = Offset.zero & size;
|
||||
if (clipRect) canvas.clipRect(canvasRect);
|
||||
scaleCanvasToViewBox(canvas, canvasRect, Offset.zero & viewBoxSize);
|
||||
drawSprites(canvas, size);
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
void scaleCanvasToViewBox(Canvas canvas, Rect canvasRect, Rect viewBoxRect) {
|
||||
final fittedSizes = applyBoxFit(fit, viewBoxRect.size, canvasRect.size);
|
||||
|
||||
// scale viewbox size (source) to canvas size (destination)
|
||||
var sx = fittedSizes.destination.width / fittedSizes.source.width;
|
||||
var sy = fittedSizes.destination.height / fittedSizes.source.height;
|
||||
final Size scaledHalfViewBoxSize =
|
||||
Size(viewBoxRect.size.width * sx, viewBoxRect.size.height * sy) / 2.0;
|
||||
final Size halfCanvasSize = canvasRect.size / 2.0;
|
||||
// center align
|
||||
final Offset shift = Offset(
|
||||
halfCanvasSize.width - scaledHalfViewBoxSize.width,
|
||||
halfCanvasSize.height - scaledHalfViewBoxSize.height,
|
||||
);
|
||||
if (shift != Offset.zero) canvas.translate(shift.dx, shift.dy);
|
||||
if (sx != 1.0 && sy != 1.0) canvas.scale(sx, sy);
|
||||
}
|
||||
|
||||
void drawSprites(Canvas canvas, Size size) {
|
||||
for (final sprite in videoItem.sprites) {
|
||||
final imageKey = sprite.imageKey;
|
||||
// var matteKey = sprite.matteKey;
|
||||
if (imageKey.isEmpty ||
|
||||
videoItem.dynamicItem.dynamicHidden[imageKey] == true) {
|
||||
continue;
|
||||
}
|
||||
final frameItem = sprite.frames[currentFrame];
|
||||
final needTransform = frameItem.hasTransform();
|
||||
final needClip = frameItem.hasClipPath();
|
||||
if (needTransform) {
|
||||
canvas.save();
|
||||
canvas.transform(Float64List.fromList(<double>[
|
||||
frameItem.transform.a,
|
||||
frameItem.transform.b,
|
||||
0.0,
|
||||
0.0,
|
||||
frameItem.transform.c,
|
||||
frameItem.transform.d,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
frameItem.transform.tx,
|
||||
frameItem.transform.ty,
|
||||
0.0,
|
||||
1.0
|
||||
]));
|
||||
}
|
||||
if (needClip) {
|
||||
canvas.save();
|
||||
canvas.clipPath(buildDPath(frameItem.clipPath));
|
||||
}
|
||||
final frameRect =
|
||||
Rect.fromLTRB(0, 0, frameItem.layout.width, frameItem.layout.height);
|
||||
final frameAlpha =
|
||||
frameItem.hasAlpha() ? (frameItem.alpha * 255).toInt() : 255;
|
||||
drawBitmap(canvas, imageKey, frameRect, frameAlpha);
|
||||
drawShape(canvas, frameItem.shapes, frameAlpha);
|
||||
// draw dynamic
|
||||
final dynamicDrawer = videoItem.dynamicItem.dynamicDrawer[imageKey];
|
||||
if (dynamicDrawer != null) {
|
||||
dynamicDrawer(canvas, currentFrame);
|
||||
}
|
||||
if (needClip) {
|
||||
canvas.restore();
|
||||
}
|
||||
if (needTransform) {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawBitmap(Canvas canvas, String imageKey, Rect frameRect, int alpha) {
|
||||
final bitmap = videoItem.dynamicItem.dynamicImages[imageKey] ??
|
||||
videoItem.bitmapCache[imageKey];
|
||||
if (bitmap == null) return;
|
||||
|
||||
final bitmapPaint = Paint();
|
||||
bitmapPaint.filterQuality = filterQuality;
|
||||
//解决bitmap锯齿问题
|
||||
bitmapPaint.isAntiAlias = true;
|
||||
bitmapPaint.color = Color.fromARGB(alpha, 0, 0, 0);
|
||||
|
||||
Rect srcRect =
|
||||
Rect.fromLTRB(0, 0, bitmap.width.toDouble(), bitmap.height.toDouble());
|
||||
Rect dstRect = frameRect;
|
||||
canvas.drawImageRect(bitmap, srcRect, dstRect, bitmapPaint);
|
||||
drawTextOnBitmap(canvas, imageKey, frameRect, alpha);
|
||||
}
|
||||
|
||||
void drawShape(Canvas canvas, List<ShapeEntity> shapes, int frameAlpha) {
|
||||
if (shapes.isEmpty) return;
|
||||
for (var shape in shapes) {
|
||||
final path = buildPath(shape);
|
||||
if (shape.hasTransform()) {
|
||||
canvas.save();
|
||||
canvas.transform(Float64List.fromList(<double>[
|
||||
shape.transform.a,
|
||||
shape.transform.b,
|
||||
0.0,
|
||||
0.0,
|
||||
shape.transform.c,
|
||||
shape.transform.d,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
shape.transform.tx,
|
||||
shape.transform.ty,
|
||||
0.0,
|
||||
1.0
|
||||
]));
|
||||
}
|
||||
|
||||
final fill = shape.styles.fill;
|
||||
if (fill.isInitialized()) {
|
||||
final paint = Paint();
|
||||
paint.isAntiAlias = true;
|
||||
paint.style = PaintingStyle.fill;
|
||||
paint.color = Color.fromARGB(
|
||||
(fill.a * frameAlpha).toInt(),
|
||||
(fill.r * 255).toInt(),
|
||||
(fill.g * 255).toInt(),
|
||||
(fill.b * 255).toInt(),
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
final strokeWidth = shape.styles.strokeWidth;
|
||||
if (strokeWidth > 0) {
|
||||
final paint = Paint();
|
||||
paint.style = PaintingStyle.stroke;
|
||||
if (shape.styles.stroke.isInitialized()) {
|
||||
paint.color = Color.fromARGB(
|
||||
(shape.styles.stroke.a * frameAlpha).toInt(),
|
||||
(shape.styles.stroke.r * 255).toInt(),
|
||||
(shape.styles.stroke.g * 255).toInt(),
|
||||
(shape.styles.stroke.b * 255).toInt(),
|
||||
);
|
||||
}
|
||||
paint.strokeWidth = strokeWidth;
|
||||
final lineCap = shape.styles.lineCap;
|
||||
switch (lineCap) {
|
||||
case ShapeEntity_ShapeStyle_LineCap.LineCap_BUTT:
|
||||
paint.strokeCap = StrokeCap.butt;
|
||||
break;
|
||||
case ShapeEntity_ShapeStyle_LineCap.LineCap_ROUND:
|
||||
paint.strokeCap = StrokeCap.round;
|
||||
break;
|
||||
case ShapeEntity_ShapeStyle_LineCap.LineCap_SQUARE:
|
||||
paint.strokeCap = StrokeCap.square;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
final lineJoin = shape.styles.lineJoin;
|
||||
switch (lineJoin) {
|
||||
case ShapeEntity_ShapeStyle_LineJoin.LineJoin_MITER:
|
||||
paint.strokeJoin = StrokeJoin.miter;
|
||||
break;
|
||||
case ShapeEntity_ShapeStyle_LineJoin.LineJoin_ROUND:
|
||||
paint.strokeJoin = StrokeJoin.round;
|
||||
break;
|
||||
case ShapeEntity_ShapeStyle_LineJoin.LineJoin_BEVEL:
|
||||
paint.strokeJoin = StrokeJoin.bevel;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
paint.strokeMiterLimit = shape.styles.miterLimit;
|
||||
List<double> lineDash = [
|
||||
shape.styles.lineDashI,
|
||||
shape.styles.lineDashII,
|
||||
shape.styles.lineDashIII
|
||||
];
|
||||
if (lineDash[0] > 0 || lineDash[1] > 0) {
|
||||
canvas.drawPath(
|
||||
dashPath(
|
||||
path,
|
||||
dashArray: CircularIntervalList([
|
||||
lineDash[0] < 1.0 ? 1.0 : lineDash[0],
|
||||
lineDash[1] < 0.1 ? 0.1 : lineDash[1],
|
||||
]),
|
||||
dashOffset: DashOffset.absolute(lineDash[2]),
|
||||
),
|
||||
paint);
|
||||
} else {
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
if (shape.hasTransform()) {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const _validMethods = 'MLHVCSQRZmlhvcsqrz';
|
||||
|
||||
Path buildPath(ShapeEntity shape) {
|
||||
final path = Path();
|
||||
if (shape.type == ShapeEntity_ShapeType.SHAPE) {
|
||||
final args = shape.shape;
|
||||
final argD = args.d;
|
||||
return buildDPath(argD, path: path);
|
||||
} else if (shape.type == ShapeEntity_ShapeType.ELLIPSE) {
|
||||
final args = shape.ellipse;
|
||||
final xv = args.x;
|
||||
final yv = args.y;
|
||||
final rxv = args.radiusX;
|
||||
final ryv = args.radiusY;
|
||||
final rect = Rect.fromLTWH(xv - rxv, yv - ryv, rxv * 2, ryv * 2);
|
||||
if (!rect.isEmpty) path.addOval(rect);
|
||||
} else if (shape.type == ShapeEntity_ShapeType.RECT) {
|
||||
final args = shape.rect;
|
||||
final xv = args.x;
|
||||
final yv = args.y;
|
||||
final wv = args.width;
|
||||
final hv = args.height;
|
||||
final crv = args.cornerRadius;
|
||||
final rrect = RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(xv, yv, wv, hv), Radius.circular(crv));
|
||||
if (!rrect.isEmpty) path.addRRect(rrect);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Path buildDPath(String argD, {Path? path}) {
|
||||
if (videoItem.pathCache[argD] != null) {
|
||||
return videoItem.pathCache[argD]!;
|
||||
}
|
||||
path ??= Path();
|
||||
final d = argD.replaceAllMapped(RegExp('([a-df-zA-Z])'), (match) {
|
||||
return "|||${match.group(1)} ";
|
||||
}).replaceAll(RegExp(","), " ");
|
||||
var currentPointX = 0.0;
|
||||
var currentPointY = 0.0;
|
||||
double? currentPointX1;
|
||||
double? currentPointY1;
|
||||
double? currentPointX2;
|
||||
double? currentPointY2;
|
||||
d.split("|||").forEach((segment) {
|
||||
if (segment.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final firstLetter = segment.substring(0, 1);
|
||||
if (_validMethods.contains(firstLetter)) {
|
||||
final args = segment.substring(1).trim().split(" ");
|
||||
if (firstLetter == "M") {
|
||||
currentPointX = double.parse(args[0]);
|
||||
currentPointY = double.parse(args[1]);
|
||||
path!.moveTo(currentPointX, currentPointY);
|
||||
} else if (firstLetter == "m") {
|
||||
currentPointX += double.parse(args[0]);
|
||||
currentPointY += double.parse(args[1]);
|
||||
path!.moveTo(currentPointX, currentPointY);
|
||||
} else if (firstLetter == "L") {
|
||||
currentPointX = double.parse(args[0]);
|
||||
currentPointY = double.parse(args[1]);
|
||||
path!.lineTo(currentPointX, currentPointY);
|
||||
} else if (firstLetter == "l") {
|
||||
currentPointX += double.parse(args[0]);
|
||||
currentPointY += double.parse(args[1]);
|
||||
path!.lineTo(currentPointX, currentPointY);
|
||||
} else if (firstLetter == "H") {
|
||||
currentPointX = double.parse(args[0]);
|
||||
path!.lineTo(currentPointX, currentPointY);
|
||||
} else if (firstLetter == "h") {
|
||||
currentPointX += double.parse(args[0]);
|
||||
path!.lineTo(currentPointX, currentPointY);
|
||||
} else if (firstLetter == "V") {
|
||||
currentPointY = double.parse(args[0]);
|
||||
path!.lineTo(currentPointX, currentPointY);
|
||||
} else if (firstLetter == "v") {
|
||||
currentPointY += double.parse(args[0]);
|
||||
path!.lineTo(currentPointX, currentPointY);
|
||||
} else if (firstLetter == "C") {
|
||||
currentPointX1 = double.parse(args[0]);
|
||||
currentPointY1 = double.parse(args[1]);
|
||||
currentPointX2 = double.parse(args[2]);
|
||||
currentPointY2 = double.parse(args[3]);
|
||||
currentPointX = double.parse(args[4]);
|
||||
currentPointY = double.parse(args[5]);
|
||||
path!.cubicTo(
|
||||
currentPointX1!,
|
||||
currentPointY1!,
|
||||
currentPointX2!,
|
||||
currentPointY2!,
|
||||
currentPointX,
|
||||
currentPointY,
|
||||
);
|
||||
} else if (firstLetter == "c") {
|
||||
currentPointX1 = currentPointX + double.parse(args[0]);
|
||||
currentPointY1 = currentPointY + double.parse(args[1]);
|
||||
currentPointX2 = currentPointX + double.parse(args[2]);
|
||||
currentPointY2 = currentPointY + double.parse(args[3]);
|
||||
currentPointX += double.parse(args[4]);
|
||||
currentPointY += double.parse(args[5]);
|
||||
path!.cubicTo(
|
||||
currentPointX1!,
|
||||
currentPointY1!,
|
||||
currentPointX2!,
|
||||
currentPointY2!,
|
||||
currentPointX,
|
||||
currentPointY,
|
||||
);
|
||||
} else if (firstLetter == "S") {
|
||||
if (currentPointX1 != null &&
|
||||
currentPointY1 != null &&
|
||||
currentPointX2 != null &&
|
||||
currentPointY2 != null) {
|
||||
currentPointX1 = currentPointX - currentPointX2! + currentPointX;
|
||||
currentPointY1 = currentPointY - currentPointY2! + currentPointY;
|
||||
currentPointX2 = double.parse(args[0]);
|
||||
currentPointY2 = double.parse(args[1]);
|
||||
currentPointX = double.parse(args[2]);
|
||||
currentPointY = double.parse(args[3]);
|
||||
path!.cubicTo(
|
||||
currentPointX1!,
|
||||
currentPointY1!,
|
||||
currentPointX2!,
|
||||
currentPointY2!,
|
||||
currentPointX,
|
||||
currentPointY,
|
||||
);
|
||||
} else {
|
||||
currentPointX1 = double.parse(args[0]);
|
||||
currentPointY1 = double.parse(args[1]);
|
||||
currentPointX = double.parse(args[2]);
|
||||
currentPointY = double.parse(args[3]);
|
||||
path!.quadraticBezierTo(
|
||||
currentPointX1!, currentPointY1!, currentPointX, currentPointY);
|
||||
}
|
||||
} else if (firstLetter == "s") {
|
||||
if (currentPointX1 != null &&
|
||||
currentPointY1 != null &&
|
||||
currentPointX2 != null &&
|
||||
currentPointY2 != null) {
|
||||
currentPointX1 = currentPointX - currentPointX2! + currentPointX;
|
||||
currentPointY1 = currentPointY - currentPointY2! + currentPointY;
|
||||
currentPointX2 = currentPointX + double.parse(args[0]);
|
||||
currentPointY2 = currentPointY + double.parse(args[1]);
|
||||
currentPointX += double.parse(args[2]);
|
||||
currentPointY += double.parse(args[3]);
|
||||
path!.cubicTo(
|
||||
currentPointX1!,
|
||||
currentPointY1!,
|
||||
currentPointX2!,
|
||||
currentPointY2!,
|
||||
currentPointX,
|
||||
currentPointY,
|
||||
);
|
||||
} else {
|
||||
currentPointX1 = currentPointX + double.parse(args[0]);
|
||||
currentPointY1 = currentPointY + double.parse(args[1]);
|
||||
currentPointX += double.parse(args[2]);
|
||||
currentPointY += double.parse(args[3]);
|
||||
path!.quadraticBezierTo(
|
||||
currentPointX1!,
|
||||
currentPointY1!,
|
||||
currentPointX,
|
||||
currentPointY,
|
||||
);
|
||||
}
|
||||
} else if (firstLetter == "Q") {
|
||||
currentPointX1 = double.parse(args[0]);
|
||||
currentPointY1 = double.parse(args[1]);
|
||||
currentPointX = double.parse(args[2]);
|
||||
currentPointY = double.parse(args[3]);
|
||||
path!.quadraticBezierTo(
|
||||
currentPointX1!, currentPointY1!, currentPointX, currentPointY);
|
||||
} else if (firstLetter == "q") {
|
||||
currentPointX1 = currentPointX + double.parse(args[0]);
|
||||
currentPointY1 = currentPointY + double.parse(args[1]);
|
||||
currentPointX += double.parse(args[2]);
|
||||
currentPointY += double.parse(args[3]);
|
||||
path!.quadraticBezierTo(
|
||||
currentPointX1!,
|
||||
currentPointY1!,
|
||||
currentPointX,
|
||||
currentPointY,
|
||||
);
|
||||
} else if (firstLetter == "Z" || firstLetter == "z") {
|
||||
path!.close();
|
||||
}
|
||||
}
|
||||
videoItem.pathCache[argD] = path!;
|
||||
});
|
||||
return path;
|
||||
}
|
||||
|
||||
void drawTextOnBitmap(
|
||||
Canvas canvas, String imageKey, Rect frameRect, int frameAlpha) {
|
||||
var dynamicText = videoItem.dynamicItem.dynamicText;
|
||||
if (dynamicText.isEmpty) return;
|
||||
if (dynamicText[imageKey] == null) return;
|
||||
|
||||
TextPainter? textPainter = dynamicText[imageKey];
|
||||
|
||||
textPainter?.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
(frameRect.width - textPainter.width) / 2.0,
|
||||
(frameRect.height - textPainter.height) / 2.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_SVGAPainter oldDelegate) {
|
||||
if (controller._canvasNeedsClear == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !(oldDelegate.controller == controller &&
|
||||
oldDelegate.controller.videoItem == controller.videoItem &&
|
||||
oldDelegate.fit == fit &&
|
||||
oldDelegate.filterQuality == filterQuality &&
|
||||
oldDelegate.clipRect == clipRect);
|
||||
}
|
||||
}
|
||||
184
local_packages/flutter_svga-0.0.13-patched/lib/src/parser.dart
Normal file
@ -0,0 +1,184 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:archive/archive.dart' as archive;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart' show decodeImageFromList;
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:http/http.dart' show get;
|
||||
|
||||
import 'cache.dart';
|
||||
import 'proto/svga.pbserver.dart';
|
||||
|
||||
const _filterKey = 'SVGAParser';
|
||||
|
||||
/// You use SVGAParser to load and decode animation files.
|
||||
class SVGAParser {
|
||||
const SVGAParser();
|
||||
static const shared = SVGAParser();
|
||||
|
||||
/// Download animation file from remote server, and decode it.
|
||||
/// Automatically uses cache if available and enabled.
|
||||
Future<MovieEntity> decodeFromURL(String url) async {
|
||||
// Try to get from cache first
|
||||
final cachedBytes = await SVGACache.shared.getRawBytes(url);
|
||||
if (cachedBytes != null) {
|
||||
return decodeFromBuffer(cachedBytes);
|
||||
}
|
||||
|
||||
// Download and cache
|
||||
final response = await get(Uri.parse(url));
|
||||
final bytes = response.bodyBytes;
|
||||
|
||||
// Cache the raw response bytes for future use
|
||||
await SVGACache.shared.putRawBytes(url, Uint8List.fromList(bytes));
|
||||
|
||||
return decodeFromBuffer(bytes);
|
||||
}
|
||||
|
||||
/// Download animation file from bundle assets, and decode it.
|
||||
/// Automatically uses cache if available and enabled.
|
||||
Future<MovieEntity> decodeFromAssets(String path) async {
|
||||
// Try to get from cache first
|
||||
final cachedBytes = await SVGACache.shared.getRawBytes('assets:$path');
|
||||
if (cachedBytes != null) {
|
||||
return decodeFromBuffer(cachedBytes);
|
||||
}
|
||||
|
||||
// Load from assets and cache
|
||||
final byteData = await rootBundle.load(path);
|
||||
final bytes = byteData.buffer.asUint8List();
|
||||
|
||||
// Cache the asset bytes for future use
|
||||
await SVGACache.shared.putRawBytes('assets:$path', bytes);
|
||||
|
||||
return decodeFromBuffer(bytes);
|
||||
}
|
||||
|
||||
/// Download animation file from buffer, and decode it.
|
||||
Future<MovieEntity> decodeFromBuffer(List<int> bytes) {
|
||||
TimelineTask? timeline;
|
||||
if (!kReleaseMode) {
|
||||
timeline = TimelineTask(filterKey: _filterKey)
|
||||
..start('DecodeFromBuffer', arguments: {'length': bytes.length});
|
||||
}
|
||||
final inflatedBytes = const archive.ZLibDecoder().decodeBytes(bytes);
|
||||
if (timeline != null) {
|
||||
timeline.instant(
|
||||
'MovieEntity.fromBuffer()',
|
||||
arguments: {'inflatedLength': inflatedBytes.length},
|
||||
);
|
||||
}
|
||||
final movie = MovieEntity.fromBuffer(inflatedBytes);
|
||||
if (timeline != null) {
|
||||
timeline.instant(
|
||||
'prepareResources()',
|
||||
arguments: {'images': movie.images.keys.join(',')},
|
||||
);
|
||||
}
|
||||
return _prepareResources(
|
||||
_processShapeItems(movie),
|
||||
timeline: timeline,
|
||||
).whenComplete(() {
|
||||
if (timeline != null) timeline.finish();
|
||||
});
|
||||
}
|
||||
|
||||
MovieEntity _processShapeItems(MovieEntity movieItem) {
|
||||
for (var sprite in movieItem.sprites) {
|
||||
List<ShapeEntity>? lastShape;
|
||||
for (var frame in sprite.frames) {
|
||||
if (frame.shapes.isNotEmpty && frame.shapes.isNotEmpty) {
|
||||
if (frame.shapes[0].type == ShapeEntity_ShapeType.KEEP &&
|
||||
lastShape != null) {
|
||||
frame.shapes = lastShape;
|
||||
} else if (frame.shapes.isNotEmpty == true) {
|
||||
lastShape = frame.shapes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return movieItem;
|
||||
}
|
||||
|
||||
Future<MovieEntity> _prepareResources(
|
||||
MovieEntity movieItem, {
|
||||
TimelineTask? timeline,
|
||||
}) {
|
||||
final images = movieItem.images;
|
||||
if (images.isEmpty) {
|
||||
movieItem.releaseMemory();
|
||||
return Future.value(movieItem);
|
||||
}
|
||||
return Future.wait(
|
||||
images.entries.map((item) async {
|
||||
// result null means a decoding error occurred
|
||||
Uint8List data = Uint8List.fromList(item.value);
|
||||
if (isMP3Data(data)) {
|
||||
movieItem.audiosData[item.key] = data;
|
||||
} else {
|
||||
final decodeImage = await _decodeImageItem(
|
||||
item.key,
|
||||
data,
|
||||
timeline: timeline,
|
||||
);
|
||||
if (decodeImage != null) {
|
||||
movieItem.bitmapCache[item.key] = decodeImage;
|
||||
}
|
||||
}
|
||||
}),
|
||||
).then((_) {
|
||||
// Release protobuf internal structures to reduce memory
|
||||
movieItem.releaseMemory();
|
||||
return movieItem;
|
||||
});
|
||||
}
|
||||
|
||||
Future<ui.Image?> _decodeImageItem(
|
||||
String key,
|
||||
Uint8List bytes, {
|
||||
TimelineTask? timeline,
|
||||
}) async {
|
||||
TimelineTask? task;
|
||||
if (!kReleaseMode) {
|
||||
task = TimelineTask(filterKey: _filterKey, parent: timeline)
|
||||
..start('DecodeImage', arguments: {'key': key, 'length': bytes.length});
|
||||
}
|
||||
try {
|
||||
final image = await decodeImageFromList(bytes);
|
||||
if (task != null) {
|
||||
task.finish(arguments: {'imageSize': '${image.width}x${image.height}'});
|
||||
}
|
||||
return image;
|
||||
} catch (e, stack) {
|
||||
if (task != null) {
|
||||
task.finish(arguments: {'error': '$e', 'stack': '$stack'});
|
||||
}
|
||||
assert(() {
|
||||
FlutterError.reportError(
|
||||
FlutterErrorDetails(
|
||||
exception: e,
|
||||
stack: stack,
|
||||
library: 'svgaplayer',
|
||||
context: ErrorDescription('during prepare resource'),
|
||||
informationCollector: () sync* {
|
||||
yield ErrorSummary('Decoding image failed.');
|
||||
},
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool isMP3Data(Uint8List data) {
|
||||
const mp3MagicNumber = 'ID3';
|
||||
bool result = false;
|
||||
if (String.fromCharCodes(data.take(mp3MagicNumber.length)) ==
|
||||
mp3MagicNumber) {
|
||||
result = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
295
local_packages/flutter_svga-0.0.13-patched/lib/src/player.dart
Normal file
@ -0,0 +1,295 @@
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_svga/src/audio_layer.dart';
|
||||
import 'package:path_drawing/path_drawing.dart';
|
||||
|
||||
import 'parser.dart';
|
||||
import 'proto/svga.pbserver.dart';
|
||||
|
||||
part 'easy_player.dart';
|
||||
part 'painter.dart';
|
||||
|
||||
class SVGAImage extends StatefulWidget {
|
||||
final SVGAAnimationController _controller;
|
||||
final BoxFit fit;
|
||||
final bool clearsAfterStop;
|
||||
|
||||
/// Used to set the filterQuality of drawing the images inside SVGA.
|
||||
///
|
||||
/// Defaults to [FilterQuality.low]
|
||||
final FilterQuality filterQuality;
|
||||
|
||||
/// If `true`, the SVGA painter may draw beyond the expected canvas bounds
|
||||
/// and cause additional memory overhead.
|
||||
///
|
||||
/// For backwards compatibility, defaults to `null`,
|
||||
/// which means allow drawing to overflow canvas bounds.
|
||||
final bool? allowDrawingOverflow;
|
||||
|
||||
/// If `null`, the viewbox size of [MovieEntity] will be use.
|
||||
///
|
||||
/// Defaults to null.
|
||||
final Size? preferredSize;
|
||||
const SVGAImage(
|
||||
this._controller, {
|
||||
super.key,
|
||||
this.fit = BoxFit.contain,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
this.allowDrawingOverflow,
|
||||
this.clearsAfterStop = true,
|
||||
this.preferredSize,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SVGAImageState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<Listenable>('controller', _controller));
|
||||
}
|
||||
}
|
||||
|
||||
class SVGAAnimationController extends AnimationController {
|
||||
MovieEntity? _videoItem;
|
||||
final List<SVGAAudioLayer> _audioLayers = [];
|
||||
bool _canvasNeedsClear = false;
|
||||
double _volume = 1.0;
|
||||
bool _muted = false;
|
||||
|
||||
SVGAAnimationController({required super.vsync})
|
||||
: super(duration: Duration.zero);
|
||||
|
||||
/// Audio volume level (0.0 to 1.0).
|
||||
/// Default is 1.0 (full volume).
|
||||
double get volume => _volume;
|
||||
set volume(double value) {
|
||||
if (_isDisposed) return;
|
||||
_volume = value.clamp(0.0, 1.0);
|
||||
for (final audio in _audioLayers) {
|
||||
audio.setVolume(_muted ? 0.0 : _volume);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether audio is muted.
|
||||
/// When muted, volume is set to 0 but the volume setting is preserved.
|
||||
bool get muted => _muted;
|
||||
set muted(bool value) {
|
||||
if (_isDisposed) return;
|
||||
_muted = value;
|
||||
for (final audio in _audioLayers) {
|
||||
audio.setVolume(_muted ? 0.0 : _volume);
|
||||
}
|
||||
}
|
||||
|
||||
void _disposeAudioLayers() {
|
||||
if (_audioLayers.isEmpty) return;
|
||||
final audioLayers = List<SVGAAudioLayer>.of(_audioLayers);
|
||||
_audioLayers.clear();
|
||||
for (final audio in audioLayers) {
|
||||
audio.stopAudio();
|
||||
unawaited(audio.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
set videoItem(MovieEntity? value) {
|
||||
assert(!_isDisposed, '$this has been disposed!');
|
||||
if (_isDisposed) return;
|
||||
if (isAnimating) {
|
||||
stop();
|
||||
}
|
||||
_disposeAudioLayers();
|
||||
if (value == null) {
|
||||
clear();
|
||||
}
|
||||
if (_videoItem != null && _videoItem!.autorelease) {
|
||||
_videoItem!.dispose();
|
||||
}
|
||||
_videoItem = value;
|
||||
if (value != null) {
|
||||
final movieParams = value.params;
|
||||
assert(
|
||||
movieParams.viewBoxWidth >= 0 &&
|
||||
movieParams.viewBoxHeight >= 0 &&
|
||||
movieParams.frames >= 1,
|
||||
"Invalid SVGA file!",
|
||||
);
|
||||
int fps = movieParams.fps;
|
||||
// avoid dividing by 0, use 20 by default
|
||||
// see https://github.com/svga/SVGAPlayer-Web/blob/1c5711db068a25006316f9890b11d6666d531c39/src/videoEntity.js#L51
|
||||
if (fps == 0) fps = 20;
|
||||
duration = Duration(
|
||||
milliseconds: (movieParams.frames / fps * 1000).toInt(),
|
||||
);
|
||||
|
||||
for (var audio in value.audios) {
|
||||
final audioLayer = SVGAAudioLayer(audio, value);
|
||||
audioLayer.setVolume(_muted ? 0.0 : _volume);
|
||||
_audioLayers.add(audioLayer);
|
||||
}
|
||||
} else {
|
||||
duration = Duration.zero;
|
||||
}
|
||||
// reset progress after videoitem changed
|
||||
reset();
|
||||
}
|
||||
|
||||
MovieEntity? get videoItem => _videoItem;
|
||||
|
||||
/// Current drawing frame index of [videoItem], returns 0 if [videoItem] is null.
|
||||
int get currentFrame {
|
||||
final videoItem = _videoItem;
|
||||
if (videoItem == null) return 0;
|
||||
return min(
|
||||
videoItem.params.frames - 1,
|
||||
max(0, (videoItem.params.frames.toDouble() * value).toInt()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Total frames of [videoItem], returns 0 if [videoItem] is null.
|
||||
int get frames {
|
||||
final videoItem = _videoItem;
|
||||
if (videoItem == null) return 0;
|
||||
return videoItem.params.frames;
|
||||
}
|
||||
|
||||
/// mark [_SVGAPainter] needs clear
|
||||
void clear() {
|
||||
_canvasNeedsClear = true;
|
||||
if (!_isDisposed) notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
TickerFuture forward({double? from}) {
|
||||
assert(
|
||||
_videoItem != null,
|
||||
'SVGAAnimationController.forward() called after dispose()?',
|
||||
);
|
||||
return super.forward(from: from);
|
||||
}
|
||||
|
||||
@override
|
||||
void stop({bool canceled = true}) {
|
||||
for (final audio in _audioLayers) {
|
||||
audio.pauseAudio();
|
||||
}
|
||||
super.stop(canceled: canceled);
|
||||
}
|
||||
|
||||
bool _isDisposed = false;
|
||||
@override
|
||||
void dispose() {
|
||||
// auto dispose _videoItem when set null
|
||||
videoItem = null;
|
||||
_isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _SVGAImageState extends State<SVGAImage> {
|
||||
MovieEntity? video;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
video = widget._controller.videoItem;
|
||||
widget._controller.addListener(_handleChange);
|
||||
widget._controller.addStatusListener(_handleStatusChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SVGAImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget._controller != widget._controller) {
|
||||
oldWidget._controller.removeListener(_handleChange);
|
||||
oldWidget._controller.removeStatusListener(_handleStatusChange);
|
||||
video = widget._controller.videoItem;
|
||||
widget._controller.addListener(_handleChange);
|
||||
widget._controller.addStatusListener(_handleStatusChange);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleChange() {
|
||||
if (mounted) {
|
||||
if (video == widget._controller.videoItem) {
|
||||
handleAudio();
|
||||
} else if (!widget._controller._isDisposed) {
|
||||
setState(() {
|
||||
// rebuild
|
||||
video = widget._controller.videoItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleStatusChange(AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed && widget.clearsAfterStop) {
|
||||
widget._controller.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void handleAudio() {
|
||||
final controller = widget._controller;
|
||||
final audioLayers = controller._audioLayers;
|
||||
final effectiveVolume = controller._muted ? 0.0 : controller._volume;
|
||||
for (final audio in audioLayers) {
|
||||
if (!audio.isPlaying() &&
|
||||
audio.audioItem.startFrame <= controller.currentFrame &&
|
||||
audio.audioItem.endFrame >= controller.currentFrame) {
|
||||
audio.playAudio(volume: effectiveVolume);
|
||||
}
|
||||
if (audio.isPlaying() &&
|
||||
audio.audioItem.endFrame <= controller.currentFrame) {
|
||||
audio.stopAudio();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
video = null;
|
||||
widget._controller.removeListener(_handleChange);
|
||||
widget._controller.removeStatusListener(_handleStatusChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final video = this.video;
|
||||
final Size viewBoxSize;
|
||||
if (video == null || !video.isInitialized()) {
|
||||
viewBoxSize = Size.zero;
|
||||
} else {
|
||||
viewBoxSize = Size(video.params.viewBoxWidth, video.params.viewBoxHeight);
|
||||
}
|
||||
if (viewBoxSize.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// sugguest the size of CustomPaint
|
||||
Size preferredSize = viewBoxSize;
|
||||
if (widget.preferredSize != null) {
|
||||
preferredSize = BoxConstraints.tight(
|
||||
widget.preferredSize!,
|
||||
).constrain(viewBoxSize);
|
||||
}
|
||||
return IgnorePointer(
|
||||
child: CustomPaint(
|
||||
painter: _SVGAPainter(
|
||||
// _SVGAPainter will auto repaint on _controller animating
|
||||
widget._controller,
|
||||
fit: widget.fit,
|
||||
filterQuality: widget.filterQuality,
|
||||
// default is allowing overflow for backward compatibility
|
||||
clipRect: widget.allowDrawingOverflow == false,
|
||||
),
|
||||
size: preferredSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
// Generated code. Do not modify.
|
||||
// source: svga.proto
|
||||
//
|
||||
// @dart = '3.7.2'
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
|
||||
|
||||
// ignore_for_file: UNDEFINED_SHOWN_NAME
|
||||
import 'dart:core' as $core;
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class ShapeEntity_ShapeType extends $pb.ProtobufEnum {
|
||||
static const ShapeEntity_ShapeType SHAPE = ShapeEntity_ShapeType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'SHAPE');
|
||||
static const ShapeEntity_ShapeType RECT = ShapeEntity_ShapeType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'RECT');
|
||||
static const ShapeEntity_ShapeType ELLIPSE = ShapeEntity_ShapeType._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'ELLIPSE');
|
||||
static const ShapeEntity_ShapeType KEEP = ShapeEntity_ShapeType._(
|
||||
3,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'KEEP');
|
||||
|
||||
static const $core.List<ShapeEntity_ShapeType> values =
|
||||
<ShapeEntity_ShapeType>[
|
||||
SHAPE,
|
||||
RECT,
|
||||
ELLIPSE,
|
||||
KEEP,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ShapeEntity_ShapeType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static ShapeEntity_ShapeType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const ShapeEntity_ShapeType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class ShapeEntity_ShapeStyle_LineCap extends $pb.ProtobufEnum {
|
||||
static const ShapeEntity_ShapeStyle_LineCap LineCap_BUTT =
|
||||
ShapeEntity_ShapeStyle_LineCap._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'LineCap_BUTT');
|
||||
static const ShapeEntity_ShapeStyle_LineCap LineCap_ROUND =
|
||||
ShapeEntity_ShapeStyle_LineCap._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'LineCap_ROUND');
|
||||
static const ShapeEntity_ShapeStyle_LineCap LineCap_SQUARE =
|
||||
ShapeEntity_ShapeStyle_LineCap._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'LineCap_SQUARE');
|
||||
|
||||
static const $core.List<ShapeEntity_ShapeStyle_LineCap> values =
|
||||
<ShapeEntity_ShapeStyle_LineCap>[
|
||||
LineCap_BUTT,
|
||||
LineCap_ROUND,
|
||||
LineCap_SQUARE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ShapeEntity_ShapeStyle_LineCap> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static ShapeEntity_ShapeStyle_LineCap? valueOf($core.int value) =>
|
||||
_byValue[value];
|
||||
|
||||
const ShapeEntity_ShapeStyle_LineCap._($core.int v, $core.String n)
|
||||
: super(v, n);
|
||||
}
|
||||
|
||||
class ShapeEntity_ShapeStyle_LineJoin extends $pb.ProtobufEnum {
|
||||
static const ShapeEntity_ShapeStyle_LineJoin LineJoin_MITER =
|
||||
ShapeEntity_ShapeStyle_LineJoin._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'LineJoin_MITER');
|
||||
static const ShapeEntity_ShapeStyle_LineJoin LineJoin_ROUND =
|
||||
ShapeEntity_ShapeStyle_LineJoin._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'LineJoin_ROUND');
|
||||
static const ShapeEntity_ShapeStyle_LineJoin LineJoin_BEVEL =
|
||||
ShapeEntity_ShapeStyle_LineJoin._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'LineJoin_BEVEL');
|
||||
|
||||
static const $core.List<ShapeEntity_ShapeStyle_LineJoin> values =
|
||||
<ShapeEntity_ShapeStyle_LineJoin>[
|
||||
LineJoin_MITER,
|
||||
LineJoin_ROUND,
|
||||
LineJoin_BEVEL,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ShapeEntity_ShapeStyle_LineJoin> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static ShapeEntity_ShapeStyle_LineJoin? valueOf($core.int value) =>
|
||||
_byValue[value];
|
||||
|
||||
const ShapeEntity_ShapeStyle_LineJoin._($core.int v, $core.String n)
|
||||
: super(v, n);
|
||||
}
|
||||
@ -0,0 +1,368 @@
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package, constant_identifier_names
|
||||
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
|
||||
@$core.Deprecated('Use movieParamsDescriptor instead')
|
||||
const MovieParams$json = const {
|
||||
'1': 'MovieParams',
|
||||
'2': const [
|
||||
const {'1': 'viewBoxWidth', '3': 1, '4': 1, '5': 2, '10': 'viewBoxWidth'},
|
||||
const {'1': 'viewBoxHeight', '3': 2, '4': 1, '5': 2, '10': 'viewBoxHeight'},
|
||||
const {'1': 'fps', '3': 3, '4': 1, '5': 5, '10': 'fps'},
|
||||
const {'1': 'frames', '3': 4, '4': 1, '5': 5, '10': 'frames'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `MovieParams`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List movieParamsDescriptor = $convert.base64Decode(
|
||||
'CgtNb3ZpZVBhcmFtcxIiCgx2aWV3Qm94V2lkdGgYASABKAJSDHZpZXdCb3hXaWR0aBIkCg12aWV3Qm94SGVpZ2h0GAIgASgCUg12aWV3Qm94SGVpZ2h0EhAKA2ZwcxgDIAEoBVIDZnBzEhYKBmZyYW1lcxgEIAEoBVIGZnJhbWVz');
|
||||
@$core.Deprecated('Use spriteEntityDescriptor instead')
|
||||
const SpriteEntity$json = const {
|
||||
'1': 'SpriteEntity',
|
||||
'2': const [
|
||||
const {'1': 'imageKey', '3': 1, '4': 1, '5': 9, '10': 'imageKey'},
|
||||
const {
|
||||
'1': 'frames',
|
||||
'3': 2,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.FrameEntity',
|
||||
'10': 'frames'
|
||||
},
|
||||
const {'1': 'matteKey', '3': 3, '4': 1, '5': 9, '10': 'matteKey'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `SpriteEntity`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List spriteEntityDescriptor = $convert.base64Decode(
|
||||
'CgxTcHJpdGVFbnRpdHkSGgoIaW1hZ2VLZXkYASABKAlSCGltYWdlS2V5EjgKBmZyYW1lcxgCIAMoCzIgLmNvbS5vcGVuc291cmNlLnN2Z2EuRnJhbWVFbnRpdHlSBmZyYW1lcxIaCghtYXR0ZUtleRgDIAEoCVIIbWF0dGVLZXk=');
|
||||
@$core.Deprecated('Use audioEntityDescriptor instead')
|
||||
const AudioEntity$json = const {
|
||||
'1': 'AudioEntity',
|
||||
'2': const [
|
||||
const {'1': 'audioKey', '3': 1, '4': 1, '5': 9, '10': 'audioKey'},
|
||||
const {'1': 'startFrame', '3': 2, '4': 1, '5': 5, '10': 'startFrame'},
|
||||
const {'1': 'endFrame', '3': 3, '4': 1, '5': 5, '10': 'endFrame'},
|
||||
const {'1': 'startTime', '3': 4, '4': 1, '5': 5, '10': 'startTime'},
|
||||
const {'1': 'totalTime', '3': 5, '4': 1, '5': 5, '10': 'totalTime'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `AudioEntity`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List audioEntityDescriptor = $convert.base64Decode(
|
||||
'CgtBdWRpb0VudGl0eRIaCghhdWRpb0tleRgBIAEoCVIIYXVkaW9LZXkSHgoKc3RhcnRGcmFtZRgCIAEoBVIKc3RhcnRGcmFtZRIaCghlbmRGcmFtZRgDIAEoBVIIZW5kRnJhbWUSHAoJc3RhcnRUaW1lGAQgASgFUglzdGFydFRpbWUSHAoJdG90YWxUaW1lGAUgASgFUgl0b3RhbFRpbWU=');
|
||||
@$core.Deprecated('Use layoutDescriptor instead')
|
||||
const Layout$json = const {
|
||||
'1': 'Layout',
|
||||
'2': const [
|
||||
const {'1': 'x', '3': 1, '4': 1, '5': 2, '10': 'x'},
|
||||
const {'1': 'y', '3': 2, '4': 1, '5': 2, '10': 'y'},
|
||||
const {'1': 'width', '3': 3, '4': 1, '5': 2, '10': 'width'},
|
||||
const {'1': 'height', '3': 4, '4': 1, '5': 2, '10': 'height'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Layout`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List layoutDescriptor = $convert.base64Decode(
|
||||
'CgZMYXlvdXQSDAoBeBgBIAEoAlIBeBIMCgF5GAIgASgCUgF5EhQKBXdpZHRoGAMgASgCUgV3aWR0aBIWCgZoZWlnaHQYBCABKAJSBmhlaWdodA==');
|
||||
@$core.Deprecated('Use transformDescriptor instead')
|
||||
const Transform$json = const {
|
||||
'1': 'Transform',
|
||||
'2': const [
|
||||
const {'1': 'a', '3': 1, '4': 1, '5': 2, '10': 'a'},
|
||||
const {'1': 'b', '3': 2, '4': 1, '5': 2, '10': 'b'},
|
||||
const {'1': 'c', '3': 3, '4': 1, '5': 2, '10': 'c'},
|
||||
const {'1': 'd', '3': 4, '4': 1, '5': 2, '10': 'd'},
|
||||
const {'1': 'tx', '3': 5, '4': 1, '5': 2, '10': 'tx'},
|
||||
const {'1': 'ty', '3': 6, '4': 1, '5': 2, '10': 'ty'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Transform`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List transformDescriptor = $convert.base64Decode(
|
||||
'CglUcmFuc2Zvcm0SDAoBYRgBIAEoAlIBYRIMCgFiGAIgASgCUgFiEgwKAWMYAyABKAJSAWMSDAoBZBgEIAEoAlIBZBIOCgJ0eBgFIAEoAlICdHgSDgoCdHkYBiABKAJSAnR5');
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity$json = const {
|
||||
'1': 'ShapeEntity',
|
||||
'2': const [
|
||||
const {
|
||||
'1': 'type',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 14,
|
||||
'6': '.com.opensource.svga.ShapeEntity.ShapeType',
|
||||
'10': 'type'
|
||||
},
|
||||
const {
|
||||
'1': 'shape',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.ShapeEntity.ShapeArgs',
|
||||
'9': 0,
|
||||
'10': 'shape'
|
||||
},
|
||||
const {
|
||||
'1': 'rect',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.ShapeEntity.RectArgs',
|
||||
'9': 0,
|
||||
'10': 'rect'
|
||||
},
|
||||
const {
|
||||
'1': 'ellipse',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.ShapeEntity.EllipseArgs',
|
||||
'9': 0,
|
||||
'10': 'ellipse'
|
||||
},
|
||||
const {
|
||||
'1': 'styles',
|
||||
'3': 10,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.ShapeEntity.ShapeStyle',
|
||||
'10': 'styles'
|
||||
},
|
||||
const {
|
||||
'1': 'transform',
|
||||
'3': 11,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.Transform',
|
||||
'10': 'transform'
|
||||
},
|
||||
],
|
||||
'3': const [
|
||||
ShapeEntity_ShapeArgs$json,
|
||||
ShapeEntity_RectArgs$json,
|
||||
ShapeEntity_EllipseArgs$json,
|
||||
ShapeEntity_ShapeStyle$json
|
||||
],
|
||||
'4': const [ShapeEntity_ShapeType$json],
|
||||
'8': const [
|
||||
const {'1': 'args'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity_ShapeArgs$json = const {
|
||||
'1': 'ShapeArgs',
|
||||
'2': const [
|
||||
const {'1': 'd', '3': 1, '4': 1, '5': 9, '10': 'd'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity_RectArgs$json = const {
|
||||
'1': 'RectArgs',
|
||||
'2': const [
|
||||
const {'1': 'x', '3': 1, '4': 1, '5': 2, '10': 'x'},
|
||||
const {'1': 'y', '3': 2, '4': 1, '5': 2, '10': 'y'},
|
||||
const {'1': 'width', '3': 3, '4': 1, '5': 2, '10': 'width'},
|
||||
const {'1': 'height', '3': 4, '4': 1, '5': 2, '10': 'height'},
|
||||
const {'1': 'cornerRadius', '3': 5, '4': 1, '5': 2, '10': 'cornerRadius'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity_EllipseArgs$json = const {
|
||||
'1': 'EllipseArgs',
|
||||
'2': const [
|
||||
const {'1': 'x', '3': 1, '4': 1, '5': 2, '10': 'x'},
|
||||
const {'1': 'y', '3': 2, '4': 1, '5': 2, '10': 'y'},
|
||||
const {'1': 'radiusX', '3': 3, '4': 1, '5': 2, '10': 'radiusX'},
|
||||
const {'1': 'radiusY', '3': 4, '4': 1, '5': 2, '10': 'radiusY'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity_ShapeStyle$json = const {
|
||||
'1': 'ShapeStyle',
|
||||
'2': const [
|
||||
const {
|
||||
'1': 'fill',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.ShapeEntity.ShapeStyle.RGBAColor',
|
||||
'10': 'fill'
|
||||
},
|
||||
const {
|
||||
'1': 'stroke',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.ShapeEntity.ShapeStyle.RGBAColor',
|
||||
'10': 'stroke'
|
||||
},
|
||||
const {'1': 'strokeWidth', '3': 3, '4': 1, '5': 2, '10': 'strokeWidth'},
|
||||
const {
|
||||
'1': 'lineCap',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 14,
|
||||
'6': '.com.opensource.svga.ShapeEntity.ShapeStyle.LineCap',
|
||||
'10': 'lineCap'
|
||||
},
|
||||
const {
|
||||
'1': 'lineJoin',
|
||||
'3': 5,
|
||||
'4': 1,
|
||||
'5': 14,
|
||||
'6': '.com.opensource.svga.ShapeEntity.ShapeStyle.LineJoin',
|
||||
'10': 'lineJoin'
|
||||
},
|
||||
const {'1': 'miterLimit', '3': 6, '4': 1, '5': 2, '10': 'miterLimit'},
|
||||
const {'1': 'lineDashI', '3': 7, '4': 1, '5': 2, '10': 'lineDashI'},
|
||||
const {'1': 'lineDashII', '3': 8, '4': 1, '5': 2, '10': 'lineDashII'},
|
||||
const {'1': 'lineDashIII', '3': 9, '4': 1, '5': 2, '10': 'lineDashIII'},
|
||||
],
|
||||
'3': const [ShapeEntity_ShapeStyle_RGBAColor$json],
|
||||
'4': const [
|
||||
ShapeEntity_ShapeStyle_LineCap$json,
|
||||
ShapeEntity_ShapeStyle_LineJoin$json
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity_ShapeStyle_RGBAColor$json = const {
|
||||
'1': 'RGBAColor',
|
||||
'2': const [
|
||||
const {'1': 'r', '3': 1, '4': 1, '5': 2, '10': 'r'},
|
||||
const {'1': 'g', '3': 2, '4': 1, '5': 2, '10': 'g'},
|
||||
const {'1': 'b', '3': 3, '4': 1, '5': 2, '10': 'b'},
|
||||
const {'1': 'a', '3': 4, '4': 1, '5': 2, '10': 'a'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity_ShapeStyle_LineCap$json = const {
|
||||
'1': 'LineCap',
|
||||
'2': const [
|
||||
const {'1': 'LineCap_BUTT', '2': 0},
|
||||
const {'1': 'LineCap_ROUND', '2': 1},
|
||||
const {'1': 'LineCap_SQUARE', '2': 2},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity_ShapeStyle_LineJoin$json = const {
|
||||
'1': 'LineJoin',
|
||||
'2': const [
|
||||
const {'1': 'LineJoin_MITER', '2': 0},
|
||||
const {'1': 'LineJoin_ROUND', '2': 1},
|
||||
const {'1': 'LineJoin_BEVEL', '2': 2},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use shapeEntityDescriptor instead')
|
||||
const ShapeEntity_ShapeType$json = const {
|
||||
'1': 'ShapeType',
|
||||
'2': const [
|
||||
const {'1': 'SHAPE', '2': 0},
|
||||
const {'1': 'RECT', '2': 1},
|
||||
const {'1': 'ELLIPSE', '2': 2},
|
||||
const {'1': 'KEEP', '2': 3},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ShapeEntity`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List shapeEntityDescriptor = $convert.base64Decode(
|
||||
'CgtTaGFwZUVudGl0eRI+CgR0eXBlGAEgASgOMiouY29tLm9wZW5zb3VyY2Uuc3ZnYS5TaGFwZUVudGl0eS5TaGFwZVR5cGVSBHR5cGUSQgoFc2hhcGUYAiABKAsyKi5jb20ub3BlbnNvdXJjZS5zdmdhLlNoYXBlRW50aXR5LlNoYXBlQXJnc0gAUgVzaGFwZRI/CgRyZWN0GAMgASgLMikuY29tLm9wZW5zb3VyY2Uuc3ZnYS5TaGFwZUVudGl0eS5SZWN0QXJnc0gAUgRyZWN0EkgKB2VsbGlwc2UYBCABKAsyLC5jb20ub3BlbnNvdXJjZS5zdmdhLlNoYXBlRW50aXR5LkVsbGlwc2VBcmdzSABSB2VsbGlwc2USQwoGc3R5bGVzGAogASgLMisuY29tLm9wZW5zb3VyY2Uuc3ZnYS5TaGFwZUVudGl0eS5TaGFwZVN0eWxlUgZzdHlsZXMSPAoJdHJhbnNmb3JtGAsgASgLMh4uY29tLm9wZW5zb3VyY2Uuc3ZnYS5UcmFuc2Zvcm1SCXRyYW5zZm9ybRoZCglTaGFwZUFyZ3MSDAoBZBgBIAEoCVIBZBp4CghSZWN0QXJncxIMCgF4GAEgASgCUgF4EgwKAXkYAiABKAJSAXkSFAoFd2lkdGgYAyABKAJSBXdpZHRoEhYKBmhlaWdodBgEIAEoAlIGaGVpZ2h0EiIKDGNvcm5lclJhZGl1cxgFIAEoAlIMY29ybmVyUmFkaXVzGl0KC0VsbGlwc2VBcmdzEgwKAXgYASABKAJSAXgSDAoBeRgCIAEoAlIBeRIYCgdyYWRpdXNYGAMgASgCUgdyYWRpdXNYEhgKB3JhZGl1c1kYBCABKAJSB3JhZGl1c1kaugUKClNoYXBlU3R5bGUSSQoEZmlsbBgBIAEoCzI1LmNvbS5vcGVuc291cmNlLnN2Z2EuU2hhcGVFbnRpdHkuU2hhcGVTdHlsZS5SR0JBQ29sb3JSBGZpbGwSTQoGc3Ryb2tlGAIgASgLMjUuY29tLm9wZW5zb3VyY2Uuc3ZnYS5TaGFwZUVudGl0eS5TaGFwZVN0eWxlLlJHQkFDb2xvclIGc3Ryb2tlEiAKC3N0cm9rZVdpZHRoGAMgASgCUgtzdHJva2VXaWR0aBJNCgdsaW5lQ2FwGAQgASgOMjMuY29tLm9wZW5zb3VyY2Uuc3ZnYS5TaGFwZUVudGl0eS5TaGFwZVN0eWxlLkxpbmVDYXBSB2xpbmVDYXASUAoIbGluZUpvaW4YBSABKA4yNC5jb20ub3BlbnNvdXJjZS5zdmdhLlNoYXBlRW50aXR5LlNoYXBlU3R5bGUuTGluZUpvaW5SCGxpbmVKb2luEh4KCm1pdGVyTGltaXQYBiABKAJSCm1pdGVyTGltaXQSHAoJbGluZURhc2hJGAcgASgCUglsaW5lRGFzaEkSHgoKbGluZURhc2hJSRgIIAEoAlIKbGluZURhc2hJSRIgCgtsaW5lRGFzaElJSRgJIAEoAlILbGluZURhc2hJSUkaQwoJUkdCQUNvbG9yEgwKAXIYASABKAJSAXISDAoBZxgCIAEoAlIBZxIMCgFiGAMgASgCUgFiEgwKAWEYBCABKAJSAWEiQgoHTGluZUNhcBIQCgxMaW5lQ2FwX0JVVFQQABIRCg1MaW5lQ2FwX1JPVU5EEAESEgoOTGluZUNhcF9TUVVBUkUQAiJGCghMaW5lSm9pbhISCg5MaW5lSm9pbl9NSVRFUhAAEhIKDkxpbmVKb2luX1JPVU5EEAESEgoOTGluZUpvaW5fQkVWRUwQAiI3CglTaGFwZVR5cGUSCQoFU0hBUEUQABIICgRSRUNUEAESCwoHRUxMSVBTRRACEggKBEtFRVAQA0IGCgRhcmdz');
|
||||
@$core.Deprecated('Use frameEntityDescriptor instead')
|
||||
const FrameEntity$json = const {
|
||||
'1': 'FrameEntity',
|
||||
'2': const [
|
||||
const {'1': 'alpha', '3': 1, '4': 1, '5': 2, '10': 'alpha'},
|
||||
const {
|
||||
'1': 'layout',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.Layout',
|
||||
'10': 'layout'
|
||||
},
|
||||
const {
|
||||
'1': 'transform',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.Transform',
|
||||
'10': 'transform'
|
||||
},
|
||||
const {'1': 'clipPath', '3': 4, '4': 1, '5': 9, '10': 'clipPath'},
|
||||
const {
|
||||
'1': 'shapes',
|
||||
'3': 5,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.ShapeEntity',
|
||||
'10': 'shapes'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `FrameEntity`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List frameEntityDescriptor = $convert.base64Decode(
|
||||
'CgtGcmFtZUVudGl0eRIUCgVhbHBoYRgBIAEoAlIFYWxwaGESMwoGbGF5b3V0GAIgASgLMhsuY29tLm9wZW5zb3VyY2Uuc3ZnYS5MYXlvdXRSBmxheW91dBI8Cgl0cmFuc2Zvcm0YAyABKAsyHi5jb20ub3BlbnNvdXJjZS5zdmdhLlRyYW5zZm9ybVIJdHJhbnNmb3JtEhoKCGNsaXBQYXRoGAQgASgJUghjbGlwUGF0aBI4CgZzaGFwZXMYBSADKAsyIC5jb20ub3BlbnNvdXJjZS5zdmdhLlNoYXBlRW50aXR5UgZzaGFwZXM=');
|
||||
@$core.Deprecated('Use movieEntityDescriptor instead')
|
||||
const MovieEntity$json = const {
|
||||
'1': 'MovieEntity',
|
||||
'2': const [
|
||||
const {'1': 'version', '3': 1, '4': 1, '5': 9, '10': 'version'},
|
||||
const {
|
||||
'1': 'params',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.MovieParams',
|
||||
'10': 'params'
|
||||
},
|
||||
const {
|
||||
'1': 'images',
|
||||
'3': 3,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.MovieEntity.ImagesEntry',
|
||||
'10': 'images'
|
||||
},
|
||||
const {
|
||||
'1': 'sprites',
|
||||
'3': 4,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.SpriteEntity',
|
||||
'10': 'sprites'
|
||||
},
|
||||
const {
|
||||
'1': 'audios',
|
||||
'3': 5,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.com.opensource.svga.AudioEntity',
|
||||
'10': 'audios'
|
||||
},
|
||||
],
|
||||
'3': const [MovieEntity_ImagesEntry$json],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use movieEntityDescriptor instead')
|
||||
const MovieEntity_ImagesEntry$json = const {
|
||||
'1': 'ImagesEntry',
|
||||
'2': const [
|
||||
const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'},
|
||||
const {'1': 'value', '3': 2, '4': 1, '5': 12, '10': 'value'},
|
||||
],
|
||||
'7': const {'7': true},
|
||||
};
|
||||
|
||||
/// Descriptor for `MovieEntity`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List movieEntityDescriptor = $convert.base64Decode(
|
||||
'CgtNb3ZpZUVudGl0eRIYCgd2ZXJzaW9uGAEgASgJUgd2ZXJzaW9uEjgKBnBhcmFtcxgCIAEoCzIgLmNvbS5vcGVuc291cmNlLnN2Z2EuTW92aWVQYXJhbXNSBnBhcmFtcxJECgZpbWFnZXMYAyADKAsyLC5jb20ub3BlbnNvdXJjZS5zdmdhLk1vdmllRW50aXR5LkltYWdlc0VudHJ5UgZpbWFnZXMSOwoHc3ByaXRlcxgEIAMoCzIhLmNvbS5vcGVuc291cmNlLnN2Z2EuU3ByaXRlRW50aXR5UgdzcHJpdGVzEjgKBmF1ZGlvcxgFIAMoCzIgLmNvbS5vcGVuc291cmNlLnN2Z2EuQXVkaW9FbnRpdHlSBmF1ZGlvcxo5CgtJbWFnZXNFbnRyeRIQCgNrZXkYASABKAlSA2tleRIUCgV2YWx1ZRgCIAEoDFIFdmFsdWU6AjgB');
|
||||
@ -0,0 +1,3 @@
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
|
||||
|
||||
export 'svga.pb.dart';
|
||||
34
local_packages/flutter_svga-0.0.13-patched/pubspec.yaml
Normal file
@ -0,0 +1,34 @@
|
||||
name: flutter_svga
|
||||
description: "Flutter SVGA player with intelligent caching, audio support, and dynamic elements. Active replacement for archived svgaplayer_flutter."
|
||||
version: 0.0.13
|
||||
|
||||
homepage: https://github.com/5alafawyyy/flutter_svga
|
||||
repository: https://github.com/5alafawyyy/flutter_svga
|
||||
|
||||
topics:
|
||||
- animation
|
||||
- svga
|
||||
- vector-graphics
|
||||
- lottie-alternative
|
||||
- ui-effects
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.7
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
archive: ^4.0.7
|
||||
http: ^1.6.0
|
||||
path_drawing: ^1.0.1
|
||||
protobuf: ^6.0.0
|
||||
audioplayers: ^6.5.1
|
||||
path_provider: ^2.1.5
|
||||
crypto: ^3.0.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_svga/flutter_svga.dart';
|
||||
|
||||
void main() {
|
||||
group('flutter_svga', () {
|
||||
test('SVGAAnimationController creation', () {
|
||||
// Test that we can create an animation controller
|
||||
// This is a basic test to ensure the package loads correctly
|
||||
expect(true, isTrue);
|
||||
});
|
||||
|
||||
test('SVGAParser shared instance', () {
|
||||
// Test that SVGAParser.shared is accessible
|
||||
expect(SVGAParser.shared, isNotNull);
|
||||
});
|
||||
|
||||
test('SVGAImage widget creation', () {
|
||||
// Test that SVGAImage widget can be referenced
|
||||
expect(SVGAImage, isNotNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -74,7 +74,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
audioplayers:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audioplayers
|
||||
sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70
|
||||
@ -638,10 +638,9 @@ packages:
|
||||
flutter_svga:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svga
|
||||
sha256: c914ba2aee5e2d53775b64c7ff6530b4cdc3c27fd9348debb0d86fd68b869c39
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
path: "local_packages/flutter_svga-0.0.13-patched"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.13"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
|
||||
@ -96,7 +96,8 @@ dependencies:
|
||||
tancent_vap:
|
||||
path: ./local_packages/tancent_vap-1.0.0+1
|
||||
#svga特效
|
||||
flutter_svga: ^0.0.5
|
||||
flutter_svga:
|
||||
path: ./local_packages/flutter_svga-0.0.13-patched
|
||||
#banner
|
||||
carousel_slider: ^5.1.1
|
||||
#谷歌支付
|
||||
@ -115,6 +116,7 @@ dependencies:
|
||||
#图片压缩
|
||||
flutter_image_compress: ^2.4.0
|
||||
extended_text: ^15.0.2
|
||||
audioplayers: ^6.5.1
|
||||
#缩略图
|
||||
video_thumbnail: ^0.5.6
|
||||
badges: ^3.1.2
|
||||
@ -164,6 +166,7 @@ flutter:
|
||||
- sc_images/daily_sign_in/
|
||||
- sc_images/register_reward/
|
||||
- sc_images/room/
|
||||
- sc_images/room/background_examples/
|
||||
- sc_images/room/emoji/
|
||||
- sc_images/room/entrance/
|
||||
- sc_images/room/anim/
|
||||
|
||||
BIN
sc_images/person/sc_icon_profile_card_default_bg.png
Normal file
|
After Width: | Height: | Size: 570 KiB |
BIN
sc_images/person/sc_icon_profile_chat.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
sc_images/person/sc_icon_profile_edit.png
Normal file
|
After Width: | Height: | Size: 711 B |
BIN
sc_images/room/anim/luck_gift/lucky_gift_win.mp3
Normal file
BIN
sc_images/room/background_examples/bg_example_1.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
sc_images/room/background_examples/bg_example_2.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
sc_images/room/background_examples/bg_example_3.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
sc_images/room/background_examples/bg_example_4.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
sc_images/room/background_examples/bg_example_5.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
sc_images/room/background_examples/bg_example_6.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
sc_images/room/background_examples/bg_example_7.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
sc_images/room/background_examples/bg_example_8.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
7
需求进度.md
@ -4,6 +4,13 @@
|
||||
- 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。
|
||||
- 按桌面《Android音乐添加流程.md》在 Android 语言房内补齐本地 MP3 音乐功能,并持续记录素材接入、扫描添加、列表管理、播放器和房间播放链路进度。
|
||||
|
||||
## 本轮个人资料页改版(分步重做)
|
||||
- 已按 2026-05-06 最新反馈回到旧版个人资料页结构上分步处理,不再一次性重做整页。
|
||||
- 第一步已去掉个人资料页加载阶段的骨架屏:`ref.userProfile` 或拉黑状态还没返回时,正文区域改为空占位,不再展示灰色骨架块;资料返回后仍按原来的 `Header + About/Giftwall Tab` 结构渲染。
|
||||
- 第二步已将个人资料页上方默认背景图从旧的 `sc_images/person/sc_icon_my_head_bg_defalt.png` 切换为新素材 `sc_images/person/sc_icon_profile_card_default_bg.png`;用户已有可用 `backgroundPhotos` 时仍优先展示用户自定义背景。
|
||||
- 第三步已调整头部资料区对齐:深绿资料区去掉顶部圆角改为矩形;头像/头像框移动到中间;用户名居中单独一行;ID、复制按钮、性别年龄和国旗收成同一行居中展示,复制按钮点击后仍写入剪贴板并提示 copied。
|
||||
- 验证进度:`dart format lib/modules/user/profile/person_detail_page.dart` 已执行;`flutter analyze --no-fatal-infos lib/modules/user/profile/person_detail_page.dart` 无 error/warning,仅保留该旧文件原有的 info 级代码风格提示。下一步再继续逐项对齐资料页 UI。
|
||||
|
||||
## 本轮 Android 语言房音乐功能(进行中)
|
||||
- 已查阅桌面《Android音乐添加流程.md》,并产出可执行方案文档 `docs/android-room-music-execution-plan.md`,覆盖功能范围、模块拆分、Android 权限、声网混音链路、UI 流程、风险点和 GPT 5.5 预估开发工时。
|
||||
- 已导入桌面 `音乐素材` 文件夹内的 12 张音乐相关 PNG,并统一落到 `sc_images/room/sc_music_material_*`,当前用于房间菜单入口、播放器按钮、模式切换、音量、文件夹、更多、删除和编辑等 UI 位。
|
||||
|
||||