需求
@ -5,6 +5,7 @@ import com.android.installreferrer.api.InstallReferrerStateListener
|
|||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
private var installReferrerClient: InstallReferrerClient? = null
|
private var installReferrerClient: InstallReferrerClient? = null
|
||||||
@ -20,6 +21,15 @@ class MainActivity : FlutterActivity() {
|
|||||||
else -> result.notImplemented()
|
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() {
|
override fun onDestroy() {
|
||||||
@ -79,5 +89,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val INSTALL_REFERRER_CHANNEL = "com.org.yumiparty/install_referrer"
|
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 {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
registerDurableAuthStorageChannel()
|
registerDurableAuthStorageChannel()
|
||||||
|
registerMobileContextChannel()
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
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() {
|
private func registerDurableAuthStorageChannel() {
|
||||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||||
return
|
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/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.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/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/app_localizations.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/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/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/components/appbar/socialchat_appbar.dart';
|
||||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||||
|
|
||||||
@ -30,13 +37,32 @@ class _RoomBackgroundSelectPageState extends State<RoomBackgroundSelectPage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
_tabController.addListener(_handleTabChange);
|
_tabController.addListener(_handleTabChange);
|
||||||
_officialItems = [
|
final currentBackground = _currentRoomBackground();
|
||||||
_RoomBackgroundItem(inUse: true),
|
_officialItems =
|
||||||
_RoomBackgroundItem(),
|
roomBackgroundExampleAssets
|
||||||
_RoomBackgroundItem(),
|
.map(
|
||||||
_RoomBackgroundItem(),
|
(path) => _RoomBackgroundItem(
|
||||||
];
|
imagePath: path,
|
||||||
_mineItems = [_RoomBackgroundItem(), _RoomBackgroundItem()];
|
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
|
@override
|
||||||
@ -62,35 +88,83 @@ class _RoomBackgroundSelectPageState extends State<RoomBackgroundSelectPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
for (final item in _mineItems) {
|
_markInUse(uploadedPath!);
|
||||||
item.inUse = false;
|
_upsertMineItem(uploadedPath, inUse: true);
|
||||||
}
|
|
||||||
_mineItems.insert(
|
|
||||||
0,
|
|
||||||
_RoomBackgroundItem(localImagePath: uploadedPath, inUse: true),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
_tabController.animateTo(1);
|
_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 {
|
Future<void> _previewItem(List<_RoomBackgroundItem> items, int index) async {
|
||||||
final shouldUse = await VoiceRoomRoute.openRoomBackgroundPreview<bool>(
|
final shouldUse = await VoiceRoomRoute.openRoomBackgroundPreview<bool>(
|
||||||
context,
|
context,
|
||||||
backgroundPath: items[index].localImagePath,
|
backgroundPath: items[index].imagePath,
|
||||||
);
|
);
|
||||||
if (!mounted || shouldUse != true) {
|
if (!mounted || shouldUse != true) {
|
||||||
return;
|
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
|
@override
|
||||||
@ -371,13 +445,32 @@ class _RoomBackgroundCard extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildPreview(BuildContext context) {
|
Widget _buildPreview(BuildContext context) {
|
||||||
final localizations = SCAppLocalizations.of(context)!;
|
final localizations = SCAppLocalizations.of(context)!;
|
||||||
if ((item.localImagePath ?? "").isNotEmpty) {
|
final imagePath = item.imagePath.trim();
|
||||||
final file = File(item.localImagePath!);
|
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()) {
|
if (file.existsSync()) {
|
||||||
return Image.file(file, fit: BoxFit.cover);
|
return Image.file(file, fit: BoxFit.cover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _buildEmptyPreview(localizations);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyPreview(SCAppLocalizations localizations) {
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
@ -416,8 +509,8 @@ class _RoomBackgroundCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RoomBackgroundItem {
|
class _RoomBackgroundItem {
|
||||||
_RoomBackgroundItem({this.localImagePath, this.inUse = false});
|
_RoomBackgroundItem({required this.imagePath, this.inUse = false});
|
||||||
|
|
||||||
final String? localImagePath;
|
final String imagePath;
|
||||||
bool inUse;
|
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/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||||
import 'package:yumi/app_localizations.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/components/appbar/socialchat_appbar.dart';
|
||||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||||
|
|
||||||
@ -20,8 +24,12 @@ class RoomBackgroundUploadPage extends StatefulWidget {
|
|||||||
class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
File? _selectedImage;
|
File? _selectedImage;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
Future<void> _pickImage() async {
|
Future<void> _pickImage() async {
|
||||||
|
if (_isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final pickedFile = await _picker.pickImage(
|
final pickedFile = await _picker.pickImage(
|
||||||
source: ImageSource.gallery,
|
source: ImageSource.gallery,
|
||||||
imageQuality: 90,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localizations = SCAppLocalizations.of(context)!;
|
final localizations = SCAppLocalizations.of(context)!;
|
||||||
@ -125,12 +169,18 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16.w),
|
SizedBox(height: 16.w),
|
||||||
Row(
|
SizedBox(
|
||||||
children: [
|
height: 300.w,
|
||||||
Expanded(child: _RoomBackgroundExampleCard()),
|
child: ListView.separated(
|
||||||
SizedBox(width: 16.w),
|
scrollDirection: Axis.horizontal,
|
||||||
Expanded(child: _RoomBackgroundExampleCard()),
|
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),
|
SizedBox(height: 16.w),
|
||||||
Text(
|
Text(
|
||||||
@ -144,15 +194,9 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
|||||||
SizedBox(height: 28.w),
|
SizedBox(height: 28.w),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap:
|
onTap: _selectedImage == null ? null : _saveBackground,
|
||||||
_selectedImage == null
|
|
||||||
? null
|
|
||||||
: () => SCNavigatorUtils.goBackWithParams(
|
|
||||||
context,
|
|
||||||
_selectedImage!.path,
|
|
||||||
),
|
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: _selectedImage == null ? 0.45 : 1,
|
opacity: _selectedImage == null || _isSaving ? 0.45 : 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 48.w,
|
height: 48.w,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -188,24 +232,36 @@ class _RoomBackgroundUploadPageState extends State<RoomBackgroundUploadPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RoomBackgroundExampleCard extends StatelessWidget {
|
class _RoomBackgroundExampleCard extends StatelessWidget {
|
||||||
|
const _RoomBackgroundExampleCard({required this.assetPath});
|
||||||
|
|
||||||
|
final String assetPath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localizations = SCAppLocalizations.of(context)!;
|
final localizations = SCAppLocalizations.of(context)!;
|
||||||
return Container(
|
return SizedBox(
|
||||||
|
width: 140.w,
|
||||||
height: 300.w,
|
height: 300.w,
|
||||||
decoration: BoxDecoration(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(14.w),
|
borderRadius: BorderRadius.circular(14.w),
|
||||||
color: const Color(0xff143B34),
|
|
||||||
),
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
assetPath,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(_, __, ___) => Container(
|
||||||
|
color: const Color(0xff143B34),
|
||||||
|
alignment: Alignment.center,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.image_outlined,
|
Icons.image_outlined,
|
||||||
size: 42.w,
|
size: 42.w,
|
||||||
color: Colors.white.withValues(alpha: 0.12),
|
color: Colors.white.withValues(alpha: 0.12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 12.w,
|
left: 12.w,
|
||||||
right: 12.w,
|
right: 12.w,
|
||||||
@ -218,7 +274,7 @@ class _RoomBackgroundExampleCard extends StatelessWidget {
|
|||||||
color: SocialChatTheme.primaryLight,
|
color: SocialChatTheme.primaryLight,
|
||||||
width: 1.w,
|
width: 1.w,
|
||||||
),
|
),
|
||||||
color: const Color(0xff1B4A40),
|
color: const Color(0xff1B4A40).withValues(alpha: 0.82),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
localizations.staticOrGifImage,
|
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/music/room_music_manager.dart';
|
||||||
import 'package:yumi/services/audio/rtm_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_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_network_image_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
import 'package:yumi/shared/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 roomTheme = room?.roomProps?.roomTheme;
|
||||||
final themeBack = roomTheme?.themeBack ?? "";
|
final themeBack = roomTheme?.themeBack ?? "";
|
||||||
if (themeBack.isEmpty) {
|
if (themeBack.isEmpty) {
|
||||||
@ -291,11 +297,11 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
Selector<RtcProvider, String?>(
|
Selector<RtcProvider, String?>(
|
||||||
selector:
|
selector:
|
||||||
(context, provider) =>
|
(context, provider) =>
|
||||||
_resolveRoomThemeBackground(provider.currenRoom),
|
_resolveRoomBackground(provider.currenRoom),
|
||||||
builder: (context, roomThemeBackground, child) {
|
builder: (context, roomBackground, child) {
|
||||||
return roomThemeBackground != null
|
return roomBackground != null
|
||||||
? netImage(
|
? netImage(
|
||||||
url: roomThemeBackground,
|
url: roomBackground,
|
||||||
width: ScreenUtil().screenWidth,
|
width: ScreenUtil().screenWidth,
|
||||||
height: ScreenUtil().screenHeight,
|
height: ScreenUtil().screenHeight,
|
||||||
noDefaultImg: true,
|
noDefaultImg: true,
|
||||||
@ -327,7 +333,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
const RoomBottomWidget(showGiftComboButton: false),
|
const RoomBottomWidget(showGiftComboButton: false),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
LGiftAnimalPage(),
|
PositionedDirectional(
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: RoomBottomWidget.floatingButtonHostHeight.w,
|
||||||
|
child: const IgnorePointer(child: LGiftAnimalPage()),
|
||||||
|
),
|
||||||
Transform.translate(
|
Transform.translate(
|
||||||
offset: Offset(0, -20),
|
offset: Offset(0, -20),
|
||||||
child: RoomAnimationQueueScreen(),
|
child: RoomAnimationQueueScreen(),
|
||||||
@ -569,6 +581,16 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
context,
|
context,
|
||||||
listen: false,
|
listen: false,
|
||||||
).enqueueGiftAnimation(giftModel);
|
).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) {
|
String _formatLuckyRewardAmount(num awardAmount) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:fluro/fluro.dart';
|
import 'package:fluro/fluro.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.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_preview_page.dart';
|
||||||
import 'package:yumi/modules/room/background/room_background_select_page.dart';
|
import 'package:yumi/modules/room/background/room_background_select_page.dart';
|
||||||
import 'package:yumi/modules/room/background/room_background_upload_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/them/room_theme_page.dart';
|
||||||
import 'package:yumi/modules/room/voice_room_page.dart';
|
import 'package:yumi/modules/room/voice_room_page.dart';
|
||||||
import 'package:yumi/app/routes/sc_router_init.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 {
|
class VoiceRoomRoute implements SCIRouterProvider {
|
||||||
static String voiceRoom = '/room';
|
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>(
|
static Future<T?> openVoiceRoom<T>(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
bool replace = false,
|
bool replace = false,
|
||||||
@ -107,11 +158,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
bool rootNavigator = false,
|
bool rootNavigator = false,
|
||||||
}) {
|
}) {
|
||||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
return _pushBackgroundRoute<T>(
|
||||||
|
context,
|
||||||
_buildDarkRoute<T>(
|
_buildDarkRoute<T>(
|
||||||
const RoomBackgroundSelectPage(),
|
const RoomBackgroundSelectPage(),
|
||||||
settings: RouteSettings(name: roomBackgroundSelect),
|
settings: RouteSettings(name: roomBackgroundSelect),
|
||||||
),
|
),
|
||||||
|
rootNavigator: rootNavigator,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,11 +172,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
bool rootNavigator = false,
|
bool rootNavigator = false,
|
||||||
}) {
|
}) {
|
||||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
return _pushBackgroundRoute<T>(
|
||||||
|
context,
|
||||||
_buildDarkRoute<T>(
|
_buildDarkRoute<T>(
|
||||||
const RoomBackgroundUploadPage(),
|
const RoomBackgroundUploadPage(),
|
||||||
settings: RouteSettings(name: roomBackgroundUpload),
|
settings: RouteSettings(name: roomBackgroundUpload),
|
||||||
),
|
),
|
||||||
|
rootNavigator: rootNavigator,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,11 +187,13 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
|||||||
String? backgroundPath,
|
String? backgroundPath,
|
||||||
bool rootNavigator = false,
|
bool rootNavigator = false,
|
||||||
}) {
|
}) {
|
||||||
return Navigator.of(context, rootNavigator: rootNavigator).push<T>(
|
return _pushBackgroundRoute<T>(
|
||||||
|
context,
|
||||||
_buildDarkRoute<T>(
|
_buildDarkRoute<T>(
|
||||||
RoomBackgroundPreviewPage(backgroundPath: backgroundPath),
|
RoomBackgroundPreviewPage(backgroundPath: backgroundPath),
|
||||||
settings: RouteSettings(name: roomBackgroundPreview),
|
settings: RouteSettings(name: roomBackgroundPreview),
|
||||||
),
|
),
|
||||||
|
rootNavigator: rootNavigator,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'dart:ui';
|
|||||||
import 'package:carousel_slider/carousel_slider.dart';
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.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:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:yumi/modules/user/profile/profile/sc_profile_page.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/shared/business_logic/models/res/login_res.dart';
|
||||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_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 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||||
|
|
||||||
import '../../../app/constants/sc_screen.dart';
|
import '../../../app/constants/sc_screen.dart';
|
||||||
@ -157,7 +159,6 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTab(int index, String text) {
|
Widget _buildTab(int index, String text) {
|
||||||
final isSelected = _tabController.index == index;
|
|
||||||
return Tab(text: text);
|
return Tab(text: text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +169,236 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
|||||||
super.dispose();
|
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(
|
Widget _buildProfileHeader(
|
||||||
SocialChatUserProfileManager ref,
|
SocialChatUserProfileManager ref,
|
||||||
List<PersonPhoto> backgroundPhotos,
|
List<PersonPhoto> backgroundPhotos,
|
||||||
@ -208,7 +439,7 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: 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,
|
width: ScreenUtil().screenWidth,
|
||||||
height: 300.w,
|
height: 300.w,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -219,212 +450,20 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Container(
|
child: Container(color: Color(0xff083b2f)),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xff083b2f),
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(8.w),
|
|
||||||
topRight: Radius.circular(8.w),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 210.w,
|
top: 210.w,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Column(
|
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,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
_buildHeaderAvatar(ref),
|
||||||
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",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 6.w),
|
SizedBox(height: 6.w),
|
||||||
Row(
|
_buildHeaderName(ref),
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 5.w),
|
SizedBox(height: 5.w),
|
||||||
Row(
|
_buildHeaderMeta(ref),
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 5.w),
|
SizedBox(height: 5.w),
|
||||||
Consumer<SocialChatUserProfileManager>(
|
Consumer<SocialChatUserProfileManager>(
|
||||||
builder: (context, ref, child) {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_tabs.clear();
|
_tabs.clear();
|
||||||
@ -722,7 +640,7 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: 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,
|
width: ScreenUtil().screenWidth,
|
||||||
height: 300.w,
|
height: 300.w,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -1063,7 +981,7 @@ class _PersonDetailPageState extends State<PersonDetailPage>
|
|||||||
isBlacklist
|
isBlacklist
|
||||||
? Container()
|
? Container()
|
||||||
: (isProfileLoading || isBlacklistLoading
|
: (isProfileLoading || isBlacklistLoading
|
||||||
? _buildProfileSkeleton()
|
? const SizedBox.shrink()
|
||||||
: ExtendedNestedScrollView(
|
: ExtendedNestedScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
onlyOneScrollInBody: true,
|
onlyOneScrollInBody: true,
|
||||||
|
|||||||
@ -517,7 +517,9 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 4.w),
|
SizedBox(height: 4.w),
|
||||||
Text(
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Text(
|
||||||
config?.displayNameText ?? 'VIP$level',
|
config?.displayNameText ?? 'VIP$level',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color:
|
color:
|
||||||
@ -530,6 +532,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
SizedBox(height: 6.w),
|
SizedBox(height: 6.w),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 0,
|
width: 0,
|
||||||
@ -1147,7 +1150,9 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 6.w),
|
SizedBox(width: 6.w),
|
||||||
Text(
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Text(
|
||||||
_priceText,
|
_priceText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: const Color(0xFFFFE8A7),
|
color: const Color(0xFFFFE8A7),
|
||||||
@ -1155,6 +1160,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
|||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
SCDebounceWidget(
|
SCDebounceWidget(
|
||||||
onTap: canPurchase ? _purchaseSelectedLevel : () {},
|
onTap: canPurchase ? _purchaseSelectedLevel : () {},
|
||||||
@ -1208,6 +1214,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
|||||||
return Transform.scale(
|
return Transform.scale(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@ -1234,6 +1242,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
|||||||
_buildVipLevelNumber(level),
|
_buildVipLevelNumber(level),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1362,7 +1371,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
|||||||
preview?.payableGold ?? config?.payableGold ?? config?.priceGold;
|
preview?.payableGold ?? config?.payableGold ?? config?.priceGold;
|
||||||
final days = (preview?.durationDays ?? config?.durationDays ?? 30).toInt();
|
final days = (preview?.durationDays ?? config?.durationDays ?? 30).toInt();
|
||||||
final amountText = amount == null ? '--' : _formatGold(amount);
|
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) {
|
String _formatGold(num value) {
|
||||||
|
|||||||
@ -99,6 +99,7 @@ class RoomEntryPreviewData {
|
|||||||
this.roomAccount,
|
this.roomAccount,
|
||||||
this.userId,
|
this.userId,
|
||||||
this.roomCover,
|
this.roomCover,
|
||||||
|
this.roomBackground,
|
||||||
this.roomName,
|
this.roomName,
|
||||||
this.roomDesc,
|
this.roomDesc,
|
||||||
this.event,
|
this.event,
|
||||||
@ -124,6 +125,7 @@ class RoomEntryPreviewData {
|
|||||||
roomAccount: room.roomAccount,
|
roomAccount: room.roomAccount,
|
||||||
userId: room.userId,
|
userId: room.userId,
|
||||||
roomCover: room.roomCover,
|
roomCover: room.roomCover,
|
||||||
|
roomBackground: room.roomBackground,
|
||||||
roomName: room.roomName,
|
roomName: room.roomName,
|
||||||
roomDesc: room.roomDesc,
|
roomDesc: room.roomDesc,
|
||||||
event: room.event,
|
event: room.event,
|
||||||
@ -145,6 +147,7 @@ class RoomEntryPreviewData {
|
|||||||
roomAccount: roomProfile?.roomAccount,
|
roomAccount: roomProfile?.roomAccount,
|
||||||
userId: roomProfile?.userId,
|
userId: roomProfile?.userId,
|
||||||
roomCover: roomProfile?.roomCover,
|
roomCover: roomProfile?.roomCover,
|
||||||
|
roomBackground: roomProfile?.roomBackground,
|
||||||
roomName: roomProfile?.roomName,
|
roomName: roomProfile?.roomName,
|
||||||
roomDesc: roomProfile?.roomDesc,
|
roomDesc: roomProfile?.roomDesc,
|
||||||
event: roomProfile?.event,
|
event: roomProfile?.event,
|
||||||
@ -168,6 +171,7 @@ class RoomEntryPreviewData {
|
|||||||
roomAccount: room.roomAccount,
|
roomAccount: room.roomAccount,
|
||||||
userId: room.userId,
|
userId: room.userId,
|
||||||
roomCover: room.roomCover,
|
roomCover: room.roomCover,
|
||||||
|
roomBackground: room.roomBackground,
|
||||||
roomName: room.roomName,
|
roomName: room.roomName,
|
||||||
roomDesc: room.roomDesc,
|
roomDesc: room.roomDesc,
|
||||||
event: room.event,
|
event: room.event,
|
||||||
@ -192,6 +196,7 @@ class RoomEntryPreviewData {
|
|||||||
final String? roomAccount;
|
final String? roomAccount;
|
||||||
final String? userId;
|
final String? userId;
|
||||||
final String? roomCover;
|
final String? roomCover;
|
||||||
|
final String? roomBackground;
|
||||||
final String? roomName;
|
final String? roomName;
|
||||||
final String? roomDesc;
|
final String? roomDesc;
|
||||||
final String? event;
|
final String? event;
|
||||||
@ -241,6 +246,7 @@ class RoomEntryPreviewData {
|
|||||||
nationalFlag: nationalFlag,
|
nationalFlag: nationalFlag,
|
||||||
roomAccount: roomAccount,
|
roomAccount: roomAccount,
|
||||||
roomCover: roomCover,
|
roomCover: roomCover,
|
||||||
|
roomBackground: roomBackground,
|
||||||
roomDesc: roomDesc,
|
roomDesc: roomDesc,
|
||||||
roomName: roomName,
|
roomName: roomName,
|
||||||
sysOrigin: sysOrigin,
|
sysOrigin: sysOrigin,
|
||||||
@ -1964,6 +1970,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
void updateCurrentRoomBasicInfo({
|
void updateCurrentRoomBasicInfo({
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
}) {
|
}) {
|
||||||
@ -1975,6 +1982,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
roomProfile: currenRoom?.roomProfile?.copyWith(
|
roomProfile: currenRoom?.roomProfile?.copyWith(
|
||||||
roomProfile: currentRoomProfile.copyWith(
|
roomProfile: currentRoomProfile.copyWith(
|
||||||
roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover),
|
roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover),
|
||||||
|
roomBackground: _preferNonEmpty(
|
||||||
|
roomBackground,
|
||||||
|
currentRoomProfile.roomBackground,
|
||||||
|
),
|
||||||
roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName),
|
roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName),
|
||||||
roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc),
|
roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc),
|
||||||
),
|
),
|
||||||
@ -1983,6 +1994,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
SCRoomProfileCache.saveRoomProfile(
|
SCRoomProfileCache.saveRoomProfile(
|
||||||
roomId: currentRoomProfile.id ?? "",
|
roomId: currentRoomProfile.id ?? "",
|
||||||
roomCover: roomCover,
|
roomCover: roomCover,
|
||||||
|
roomBackground: roomBackground,
|
||||||
roomName: roomName,
|
roomName: roomName,
|
||||||
roomDesc: roomDesc,
|
roomDesc: roomDesc,
|
||||||
);
|
);
|
||||||
@ -3601,7 +3613,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
final value = await SCChatRoomRepository().specific(roomId);
|
final value = await SCChatRoomRepository().specific(roomId);
|
||||||
final currentRoomProfile = currenRoom?.roomProfile?.roomProfile;
|
final currentRoomProfile = currenRoom?.roomProfile?.roomProfile;
|
||||||
debugPrint(
|
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(
|
currenRoom = currenRoom?.copyWith(
|
||||||
roomProfile: currenRoom?.roomProfile?.copyWith(
|
roomProfile: currenRoom?.roomProfile?.copyWith(
|
||||||
@ -3612,6 +3624,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
value.roomCover,
|
value.roomCover,
|
||||||
currentRoomProfile.roomCover,
|
currentRoomProfile.roomCover,
|
||||||
),
|
),
|
||||||
|
roomBackground: _preferNonEmpty(
|
||||||
|
value.roomBackground,
|
||||||
|
currentRoomProfile.roomBackground,
|
||||||
|
),
|
||||||
roomName: _preferNonEmpty(
|
roomName: _preferNonEmpty(
|
||||||
value.roomName,
|
value.roomName,
|
||||||
currentRoomProfile.roomName,
|
currentRoomProfile.roomName,
|
||||||
@ -3624,7 +3640,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
debugPrint(
|
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';
|
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
|
||||||
|
|
||||||
class GiftAnimationManager extends ChangeNotifier {
|
class GiftAnimationManager extends ChangeNotifier {
|
||||||
|
static const int maxVisibleSlots = 4;
|
||||||
static const int _maxPendingAnimations = 24;
|
static const int _maxPendingAnimations = 24;
|
||||||
static const Duration _activeComboIdleDismissDelay = Duration(
|
static const Duration _activeComboIdleDismissDelay = Duration(
|
||||||
milliseconds: 3200,
|
milliseconds: 3200,
|
||||||
@ -17,10 +18,16 @@ class GiftAnimationManager extends ChangeNotifier {
|
|||||||
//每个控件正在播放的动画
|
//每个控件正在播放的动画
|
||||||
Map<int, LGiftModel?> giftMap = {0: null, 1: null, 2: null, 3: null};
|
Map<int, LGiftModel?> giftMap = {0: null, 1: null, 2: null, 3: null};
|
||||||
|
|
||||||
|
int _visibleSlotCount = 0;
|
||||||
|
|
||||||
bool get _controllersReady =>
|
bool get _controllersReady =>
|
||||||
animationControllerList.length >= giftMap.length;
|
_visibleSlotCount <= 0 ||
|
||||||
|
animationControllerList.length >= _visibleSlotCount;
|
||||||
|
|
||||||
GiftAnimationSlotSnapshot? slotSnapshotAt(int index) {
|
GiftAnimationSlotSnapshot? slotSnapshotAt(int index) {
|
||||||
|
if (index < 0 || index >= _visibleSlotCount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
final gift = giftMap[index];
|
final gift = giftMap[index];
|
||||||
if (gift == null || animationControllerList.length <= index) {
|
if (gift == null || animationControllerList.length <= index) {
|
||||||
return null;
|
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) {
|
void enqueueGiftAnimation(LGiftModel giftModel) {
|
||||||
if (_mergeIntoActiveAnimation(giftModel)) {
|
if (_mergeIntoActiveAnimation(giftModel)) {
|
||||||
return;
|
return;
|
||||||
@ -55,21 +87,42 @@ class GiftAnimationManager extends ChangeNotifier {
|
|||||||
proceedToNextAnimation();
|
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() {
|
void _trimPendingAnimations() {
|
||||||
while (pendingAnimationsQueue.length >= _maxPendingAnimations) {
|
while (pendingAnimationsQueue.length >= _maxPendingAnimations) {
|
||||||
pendingAnimationsQueue.removeFirst();
|
pendingAnimationsQueue.removeFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _trimPendingAnimationsFromEnd() {
|
||||||
|
while (pendingAnimationsQueue.length > _maxPendingAnimations) {
|
||||||
|
pendingAnimationsQueue.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _mergeIntoActiveAnimation(LGiftModel incoming) {
|
bool _mergeIntoActiveAnimation(LGiftModel incoming) {
|
||||||
for (final entry in giftMap.entries) {
|
for (var index = 0; index < _visibleSlotCount; index += 1) {
|
||||||
final current = entry.value;
|
final current = giftMap[index];
|
||||||
if (current == null || current.labelId != incoming.labelId) {
|
if (current == null || current.labelId != incoming.labelId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_mergeGiftModel(target: current, incoming: incoming);
|
_mergeGiftModel(target: current, incoming: incoming);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_refreshSlotAnimation(entry.key, restartEntry: false);
|
_refreshSlotAnimation(index, restartEntry: false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -119,41 +172,60 @@ class GiftAnimationManager extends ChangeNotifier {
|
|||||||
|
|
||||||
///开始播放
|
///开始播放
|
||||||
proceedToNextAnimation() {
|
proceedToNextAnimation() {
|
||||||
if (pendingAnimationsQueue.isEmpty || !_controllersReady) {
|
if (pendingAnimationsQueue.isEmpty ||
|
||||||
|
!_controllersReady ||
|
||||||
|
_visibleSlotCount <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var playGift = pendingAnimationsQueue.first;
|
|
||||||
for (var key in giftMap.keys) {
|
var changed = false;
|
||||||
var value = giftMap[key];
|
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) {
|
if (value == null) {
|
||||||
giftMap[key] = playGift;
|
giftMap[index] = playGift;
|
||||||
pendingAnimationsQueue.removeFirst();
|
pendingAnimationsQueue.removeFirst();
|
||||||
notifyListeners();
|
changed = true;
|
||||||
_refreshSlotAnimation(key, restartEntry: true);
|
consumed = true;
|
||||||
|
_refreshSlotAnimation(index, restartEntry: true);
|
||||||
break;
|
break;
|
||||||
} else {
|
}
|
||||||
if (value.labelId == playGift.labelId) {
|
if (value.labelId == playGift.labelId) {
|
||||||
_mergeGiftModel(target: value, incoming: playGift);
|
_mergeGiftModel(target: value, incoming: playGift);
|
||||||
pendingAnimationsQueue.removeFirst();
|
pendingAnimationsQueue.removeFirst();
|
||||||
notifyListeners();
|
changed = true;
|
||||||
_refreshSlotAnimation(key, restartEntry: false);
|
consumed = true;
|
||||||
|
_refreshSlotAnimation(index, restartEntry: false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!consumed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void attachAnimationControllers(List<LGiftScrollingScreenAnimsBean> anins) {
|
void attachAnimationControllers(List<LGiftScrollingScreenAnimsBean> anins) {
|
||||||
animationControllerList = anins;
|
animationControllerList = anins;
|
||||||
|
if (_visibleSlotCount > animationControllerList.length) {
|
||||||
|
setVisibleSlotCount(animationControllerList.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
proceedToNextAnimation();
|
proceedToNextAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanupAnimationResources() {
|
void cleanupAnimationResources() {
|
||||||
pendingAnimationsQueue.clear();
|
pendingAnimationsQueue.clear();
|
||||||
giftMap[0] = null;
|
for (final key in giftMap.keys) {
|
||||||
giftMap[1] = null;
|
giftMap[key] = null;
|
||||||
giftMap[2] = null;
|
}
|
||||||
giftMap[3] = null;
|
_visibleSlotCount = 0;
|
||||||
for (var element in animationControllerList) {
|
for (var element in animationControllerList) {
|
||||||
element.dismissTimer?.cancel();
|
element.dismissTimer?.cancel();
|
||||||
element.controller.dispose();
|
element.controller.dispose();
|
||||||
|
|||||||
@ -43,6 +43,9 @@ class SocialChatRoomManager extends ChangeNotifier {
|
|||||||
if ((roomInfo.roomCover ?? "").trim().isNotEmpty) {
|
if ((roomInfo.roomCover ?? "").trim().isNotEmpty) {
|
||||||
myRoom?.setRoomCover = roomInfo.roomCover!;
|
myRoom?.setRoomCover = roomInfo.roomCover!;
|
||||||
}
|
}
|
||||||
|
if ((roomInfo.roomBackground ?? "").trim().isNotEmpty) {
|
||||||
|
myRoom?.setRoomBackground = roomInfo.roomBackground!;
|
||||||
|
}
|
||||||
if (roomInfo.roomName != null) {
|
if (roomInfo.roomName != null) {
|
||||||
myRoom?.setRoomName = roomInfo.roomName!;
|
myRoom?.setRoomName = roomInfo.roomName!;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,7 @@ class RoomProfile {
|
|||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
List<String>? roomBadgeIcons,
|
List<String>? roomBadgeIcons,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? roomGameIcon,
|
String? roomGameIcon,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
@ -85,6 +86,7 @@ class RoomProfile {
|
|||||||
_roomAccount = roomAccount;
|
_roomAccount = roomAccount;
|
||||||
_roomBadgeIcons = roomBadgeIcons;
|
_roomBadgeIcons = roomBadgeIcons;
|
||||||
_roomCover = roomCover;
|
_roomCover = roomCover;
|
||||||
|
_roomBackground = roomBackground;
|
||||||
_roomDesc = roomDesc;
|
_roomDesc = roomDesc;
|
||||||
_roomGameIcon = roomGameIcon;
|
_roomGameIcon = roomGameIcon;
|
||||||
_roomName = roomName;
|
_roomName = roomName;
|
||||||
@ -114,6 +116,7 @@ class RoomProfile {
|
|||||||
? json['roomBadgeIcons'].cast<String>()
|
? json['roomBadgeIcons'].cast<String>()
|
||||||
: [];
|
: [];
|
||||||
_roomCover = json['roomCover'] ?? json['cover'];
|
_roomCover = json['roomCover'] ?? json['cover'];
|
||||||
|
_roomBackground = json['roomBackground'];
|
||||||
_roomDesc = json['roomDesc'];
|
_roomDesc = json['roomDesc'];
|
||||||
_roomGameIcon = json['roomGameIcon'];
|
_roomGameIcon = json['roomGameIcon'];
|
||||||
_roomName = json['roomName'];
|
_roomName = json['roomName'];
|
||||||
@ -146,6 +149,7 @@ class RoomProfile {
|
|||||||
String? _roomAccount;
|
String? _roomAccount;
|
||||||
List<String>? _roomBadgeIcons;
|
List<String>? _roomBadgeIcons;
|
||||||
String? _roomCover;
|
String? _roomCover;
|
||||||
|
String? _roomBackground;
|
||||||
String? _roomDesc;
|
String? _roomDesc;
|
||||||
String? _roomGameIcon;
|
String? _roomGameIcon;
|
||||||
String? _roomName;
|
String? _roomName;
|
||||||
@ -166,6 +170,7 @@ class RoomProfile {
|
|||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
List<String>? roomBadgeIcons,
|
List<String>? roomBadgeIcons,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? roomGameIcon,
|
String? roomGameIcon,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
@ -186,6 +191,7 @@ class RoomProfile {
|
|||||||
roomAccount: roomAccount ?? _roomAccount,
|
roomAccount: roomAccount ?? _roomAccount,
|
||||||
roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons,
|
roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons,
|
||||||
roomCover: roomCover ?? _roomCover,
|
roomCover: roomCover ?? _roomCover,
|
||||||
|
roomBackground: roomBackground ?? _roomBackground,
|
||||||
roomDesc: roomDesc ?? _roomDesc,
|
roomDesc: roomDesc ?? _roomDesc,
|
||||||
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
||||||
roomName: roomName ?? _roomName,
|
roomName: roomName ?? _roomName,
|
||||||
@ -206,6 +212,7 @@ class RoomProfile {
|
|||||||
String? get roomAccount => _roomAccount;
|
String? get roomAccount => _roomAccount;
|
||||||
List<String>? get roomBadgeIcons => _roomBadgeIcons;
|
List<String>? get roomBadgeIcons => _roomBadgeIcons;
|
||||||
String? get roomCover => _roomCover;
|
String? get roomCover => _roomCover;
|
||||||
|
String? get roomBackground => _roomBackground;
|
||||||
String? get roomDesc => _roomDesc;
|
String? get roomDesc => _roomDesc;
|
||||||
String? get roomGameIcon => _roomGameIcon;
|
String? get roomGameIcon => _roomGameIcon;
|
||||||
String? get roomName => _roomName;
|
String? get roomName => _roomName;
|
||||||
@ -243,6 +250,7 @@ class RoomProfile {
|
|||||||
map['roomAccount'] = _roomAccount;
|
map['roomAccount'] = _roomAccount;
|
||||||
map['roomBadgeIcons'] = _roomBadgeIcons;
|
map['roomBadgeIcons'] = _roomBadgeIcons;
|
||||||
map['roomCover'] = _roomCover;
|
map['roomCover'] = _roomCover;
|
||||||
|
map['roomBackground'] = _roomBackground;
|
||||||
map['roomDesc'] = _roomDesc;
|
map['roomDesc'] = _roomDesc;
|
||||||
map['roomGameIcon'] = _roomGameIcon;
|
map['roomGameIcon'] = _roomGameIcon;
|
||||||
map['roomName'] = _roomName;
|
map['roomName'] = _roomName;
|
||||||
|
|||||||
@ -716,6 +716,7 @@ class RoomProfile2 {
|
|||||||
String? nationalFlag,
|
String? nationalFlag,
|
||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? sysOrigin,
|
String? sysOrigin,
|
||||||
@ -733,6 +734,7 @@ class RoomProfile2 {
|
|||||||
_nationalFlag = nationalFlag;
|
_nationalFlag = nationalFlag;
|
||||||
_roomAccount = roomAccount;
|
_roomAccount = roomAccount;
|
||||||
_roomCover = roomCover;
|
_roomCover = roomCover;
|
||||||
|
_roomBackground = roomBackground;
|
||||||
_roomDesc = roomDesc;
|
_roomDesc = roomDesc;
|
||||||
_roomName = roomName;
|
_roomName = roomName;
|
||||||
_sysOrigin = sysOrigin;
|
_sysOrigin = sysOrigin;
|
||||||
@ -752,6 +754,7 @@ class RoomProfile2 {
|
|||||||
_nationalFlag = json['nationalFlag'];
|
_nationalFlag = json['nationalFlag'];
|
||||||
_roomAccount = json['roomAccount'];
|
_roomAccount = json['roomAccount'];
|
||||||
_roomCover = json['roomCover'] ?? json['cover'];
|
_roomCover = json['roomCover'] ?? json['cover'];
|
||||||
|
_roomBackground = json['roomBackground'];
|
||||||
_roomDesc = json['roomDesc'];
|
_roomDesc = json['roomDesc'];
|
||||||
_roomName = json['roomName'];
|
_roomName = json['roomName'];
|
||||||
_sysOrigin = json['sysOrigin'];
|
_sysOrigin = json['sysOrigin'];
|
||||||
@ -770,6 +773,7 @@ class RoomProfile2 {
|
|||||||
String? _nationalFlag;
|
String? _nationalFlag;
|
||||||
String? _roomAccount;
|
String? _roomAccount;
|
||||||
String? _roomCover;
|
String? _roomCover;
|
||||||
|
String? _roomBackground;
|
||||||
String? _roomDesc;
|
String? _roomDesc;
|
||||||
String? _roomName;
|
String? _roomName;
|
||||||
String? _sysOrigin;
|
String? _sysOrigin;
|
||||||
@ -788,6 +792,7 @@ class RoomProfile2 {
|
|||||||
String? nationalFlag,
|
String? nationalFlag,
|
||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? sysOrigin,
|
String? sysOrigin,
|
||||||
@ -805,6 +810,7 @@ class RoomProfile2 {
|
|||||||
nationalFlag: nationalFlag ?? _nationalFlag,
|
nationalFlag: nationalFlag ?? _nationalFlag,
|
||||||
roomAccount: roomAccount ?? _roomAccount,
|
roomAccount: roomAccount ?? _roomAccount,
|
||||||
roomCover: roomCover ?? _roomCover,
|
roomCover: roomCover ?? _roomCover,
|
||||||
|
roomBackground: roomBackground ?? _roomBackground,
|
||||||
roomDesc: roomDesc ?? _roomDesc,
|
roomDesc: roomDesc ?? _roomDesc,
|
||||||
roomName: roomName ?? _roomName,
|
roomName: roomName ?? _roomName,
|
||||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||||
@ -833,6 +839,7 @@ class RoomProfile2 {
|
|||||||
String? get roomAccount => _roomAccount;
|
String? get roomAccount => _roomAccount;
|
||||||
|
|
||||||
String? get roomCover => _roomCover;
|
String? get roomCover => _roomCover;
|
||||||
|
String? get roomBackground => _roomBackground;
|
||||||
|
|
||||||
String? get roomDesc => _roomDesc;
|
String? get roomDesc => _roomDesc;
|
||||||
|
|
||||||
@ -857,6 +864,7 @@ class RoomProfile2 {
|
|||||||
map['nationalFlag'] = _nationalFlag;
|
map['nationalFlag'] = _nationalFlag;
|
||||||
map['roomAccount'] = _roomAccount;
|
map['roomAccount'] = _roomAccount;
|
||||||
map['roomCover'] = _roomCover;
|
map['roomCover'] = _roomCover;
|
||||||
|
map['roomBackground'] = _roomBackground;
|
||||||
map['roomDesc'] = _roomDesc;
|
map['roomDesc'] = _roomDesc;
|
||||||
map['roomName'] = _roomName;
|
map['roomName'] = _roomName;
|
||||||
map['sysOrigin'] = _sysOrigin;
|
map['sysOrigin'] = _sysOrigin;
|
||||||
|
|||||||
@ -29,6 +29,7 @@ class MyRoomRes {
|
|||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? event,
|
String? event,
|
||||||
@ -49,6 +50,7 @@ class MyRoomRes {
|
|||||||
_roomAccount = roomAccount;
|
_roomAccount = roomAccount;
|
||||||
_userId = userId;
|
_userId = userId;
|
||||||
_roomCover = roomCover;
|
_roomCover = roomCover;
|
||||||
|
_roomBackground = roomBackground;
|
||||||
_roomName = roomName;
|
_roomName = roomName;
|
||||||
_roomDesc = roomDesc;
|
_roomDesc = roomDesc;
|
||||||
_event = event;
|
_event = event;
|
||||||
@ -71,6 +73,7 @@ class MyRoomRes {
|
|||||||
_roomAccount = json['roomAccount'];
|
_roomAccount = json['roomAccount'];
|
||||||
_userId = json['userId'];
|
_userId = json['userId'];
|
||||||
_roomCover = json['roomCover'] ?? json['cover'];
|
_roomCover = json['roomCover'] ?? json['cover'];
|
||||||
|
_roomBackground = json['roomBackground'];
|
||||||
_roomName = json['roomName'];
|
_roomName = json['roomName'];
|
||||||
_roomDesc = json['roomDesc'];
|
_roomDesc = json['roomDesc'];
|
||||||
_event = json['event'];
|
_event = json['event'];
|
||||||
@ -98,6 +101,7 @@ class MyRoomRes {
|
|||||||
String? _roomAccount;
|
String? _roomAccount;
|
||||||
String? _userId;
|
String? _userId;
|
||||||
String? _roomCover;
|
String? _roomCover;
|
||||||
|
String? _roomBackground;
|
||||||
String? _roomName;
|
String? _roomName;
|
||||||
String? _roomDesc;
|
String? _roomDesc;
|
||||||
String? _event;
|
String? _event;
|
||||||
@ -118,6 +122,7 @@ class MyRoomRes {
|
|||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? event,
|
String? event,
|
||||||
@ -138,6 +143,7 @@ class MyRoomRes {
|
|||||||
roomAccount: roomAccount ?? _roomAccount,
|
roomAccount: roomAccount ?? _roomAccount,
|
||||||
userId: userId ?? _userId,
|
userId: userId ?? _userId,
|
||||||
roomCover: roomCover ?? _roomCover,
|
roomCover: roomCover ?? _roomCover,
|
||||||
|
roomBackground: roomBackground ?? _roomBackground,
|
||||||
roomName: roomName ?? _roomName,
|
roomName: roomName ?? _roomName,
|
||||||
roomDesc: roomDesc ?? _roomDesc,
|
roomDesc: roomDesc ?? _roomDesc,
|
||||||
event: event ?? _event,
|
event: event ?? _event,
|
||||||
@ -158,6 +164,7 @@ class MyRoomRes {
|
|||||||
String? get roomAccount => _roomAccount;
|
String? get roomAccount => _roomAccount;
|
||||||
String? get userId => _userId;
|
String? get userId => _userId;
|
||||||
String? get roomCover => _roomCover;
|
String? get roomCover => _roomCover;
|
||||||
|
String? get roomBackground => _roomBackground;
|
||||||
String? get roomName => _roomName;
|
String? get roomName => _roomName;
|
||||||
String? get roomDesc => _roomDesc;
|
String? get roomDesc => _roomDesc;
|
||||||
String? get event => _event;
|
String? get event => _event;
|
||||||
@ -174,6 +181,7 @@ class MyRoomRes {
|
|||||||
RoomCounter? get counter => _counter;
|
RoomCounter? get counter => _counter;
|
||||||
List<UseBadges>? get useBadges => _useBadges;
|
List<UseBadges>? get useBadges => _useBadges;
|
||||||
set setRoomCover(String value) => _roomCover = value;
|
set setRoomCover(String value) => _roomCover = value;
|
||||||
|
set setRoomBackground(String value) => _roomBackground = value;
|
||||||
set setRoomName(String value) => _roomName = value;
|
set setRoomName(String value) => _roomName = value;
|
||||||
set setRoomDesc(String value) => _roomDesc = value;
|
set setRoomDesc(String value) => _roomDesc = value;
|
||||||
|
|
||||||
@ -183,6 +191,7 @@ class MyRoomRes {
|
|||||||
map['roomAccount'] = _roomAccount;
|
map['roomAccount'] = _roomAccount;
|
||||||
map['userId'] = _userId;
|
map['userId'] = _userId;
|
||||||
map['roomCover'] = _roomCover;
|
map['roomCover'] = _roomCover;
|
||||||
|
map['roomBackground'] = _roomBackground;
|
||||||
map['roomName'] = _roomName;
|
map['roomName'] = _roomName;
|
||||||
map['roomDesc'] = _roomDesc;
|
map['roomDesc'] = _roomDesc;
|
||||||
map['event'] = _event;
|
map['event'] = _event;
|
||||||
|
|||||||
@ -30,6 +30,7 @@ class SocialChatRoomRes {
|
|||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
List<String>? roomBadgeIcons,
|
List<String>? roomBadgeIcons,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? roomGameIcon,
|
String? roomGameIcon,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
@ -50,6 +51,7 @@ class SocialChatRoomRes {
|
|||||||
_roomAccount = roomAccount;
|
_roomAccount = roomAccount;
|
||||||
_roomBadgeIcons = roomBadgeIcons;
|
_roomBadgeIcons = roomBadgeIcons;
|
||||||
_roomCover = roomCover;
|
_roomCover = roomCover;
|
||||||
|
_roomBackground = roomBackground;
|
||||||
_roomDesc = roomDesc;
|
_roomDesc = roomDesc;
|
||||||
_roomGameIcon = roomGameIcon;
|
_roomGameIcon = roomGameIcon;
|
||||||
_roomName = roomName;
|
_roomName = roomName;
|
||||||
@ -81,6 +83,7 @@ class SocialChatRoomRes {
|
|||||||
.toList()
|
.toList()
|
||||||
: null;
|
: null;
|
||||||
_roomCover = json['roomCover'] ?? json['cover'];
|
_roomCover = json['roomCover'] ?? json['cover'];
|
||||||
|
_roomBackground = json['roomBackground'];
|
||||||
_roomDesc = json['roomDesc'];
|
_roomDesc = json['roomDesc'];
|
||||||
_roomGameIcon = json['roomGameIcon'];
|
_roomGameIcon = json['roomGameIcon'];
|
||||||
_roomName = json['roomName'];
|
_roomName = json['roomName'];
|
||||||
@ -114,6 +117,7 @@ class SocialChatRoomRes {
|
|||||||
String? _roomAccount;
|
String? _roomAccount;
|
||||||
List<String>? _roomBadgeIcons;
|
List<String>? _roomBadgeIcons;
|
||||||
String? _roomCover;
|
String? _roomCover;
|
||||||
|
String? _roomBackground;
|
||||||
String? _roomDesc;
|
String? _roomDesc;
|
||||||
String? _roomGameIcon;
|
String? _roomGameIcon;
|
||||||
String? _roomName;
|
String? _roomName;
|
||||||
@ -135,6 +139,7 @@ class SocialChatRoomRes {
|
|||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
List<String>? roomBadgeIcons,
|
List<String>? roomBadgeIcons,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? roomGameIcon,
|
String? roomGameIcon,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
@ -155,6 +160,7 @@ class SocialChatRoomRes {
|
|||||||
roomAccount: roomAccount ?? _roomAccount,
|
roomAccount: roomAccount ?? _roomAccount,
|
||||||
roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons,
|
roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons,
|
||||||
roomCover: roomCover ?? _roomCover,
|
roomCover: roomCover ?? _roomCover,
|
||||||
|
roomBackground: roomBackground ?? _roomBackground,
|
||||||
roomDesc: roomDesc ?? _roomDesc,
|
roomDesc: roomDesc ?? _roomDesc,
|
||||||
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
||||||
roomName: roomName ?? _roomName,
|
roomName: roomName ?? _roomName,
|
||||||
@ -185,6 +191,7 @@ class SocialChatRoomRes {
|
|||||||
List<String>? get roomBadgeIcons => _roomBadgeIcons;
|
List<String>? get roomBadgeIcons => _roomBadgeIcons;
|
||||||
|
|
||||||
String? get roomCover => _roomCover;
|
String? get roomCover => _roomCover;
|
||||||
|
String? get roomBackground => _roomBackground;
|
||||||
|
|
||||||
String? get roomDesc => _roomDesc;
|
String? get roomDesc => _roomDesc;
|
||||||
|
|
||||||
@ -228,6 +235,7 @@ class SocialChatRoomRes {
|
|||||||
map['roomAccount'] = _roomAccount;
|
map['roomAccount'] = _roomAccount;
|
||||||
map['roomBadgeIcons'] = _roomBadgeIcons;
|
map['roomBadgeIcons'] = _roomBadgeIcons;
|
||||||
map['roomCover'] = _roomCover;
|
map['roomCover'] = _roomCover;
|
||||||
|
map['roomBackground'] = _roomBackground;
|
||||||
map['roomDesc'] = _roomDesc;
|
map['roomDesc'] = _roomDesc;
|
||||||
map['roomGameIcon'] = _roomGameIcon;
|
map['roomGameIcon'] = _roomGameIcon;
|
||||||
map['roomName'] = _roomName;
|
map['roomName'] = _roomName;
|
||||||
|
|||||||
@ -21,6 +21,7 @@ class SCEditRoomInfoRes {
|
|||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? event,
|
String? event,
|
||||||
@ -38,6 +39,7 @@ class SCEditRoomInfoRes {
|
|||||||
_roomAccount = roomAccount;
|
_roomAccount = roomAccount;
|
||||||
_userId = userId;
|
_userId = userId;
|
||||||
_roomCover = roomCover;
|
_roomCover = roomCover;
|
||||||
|
_roomBackground = roomBackground;
|
||||||
_roomName = roomName;
|
_roomName = roomName;
|
||||||
_roomDesc = roomDesc;
|
_roomDesc = roomDesc;
|
||||||
_event = event;
|
_event = event;
|
||||||
@ -57,6 +59,7 @@ class SCEditRoomInfoRes {
|
|||||||
_roomAccount = json['roomAccount'];
|
_roomAccount = json['roomAccount'];
|
||||||
_userId = json['userId'];
|
_userId = json['userId'];
|
||||||
_roomCover = json['roomCover'] ?? json['cover'];
|
_roomCover = json['roomCover'] ?? json['cover'];
|
||||||
|
_roomBackground = json['roomBackground'];
|
||||||
_roomName = json['roomName'];
|
_roomName = json['roomName'];
|
||||||
_roomDesc = json['roomDesc'];
|
_roomDesc = json['roomDesc'];
|
||||||
_event = json['event'];
|
_event = json['event'];
|
||||||
@ -74,6 +77,7 @@ class SCEditRoomInfoRes {
|
|||||||
String? _roomAccount;
|
String? _roomAccount;
|
||||||
String? _userId;
|
String? _userId;
|
||||||
String? _roomCover;
|
String? _roomCover;
|
||||||
|
String? _roomBackground;
|
||||||
String? _roomName;
|
String? _roomName;
|
||||||
String? _roomDesc;
|
String? _roomDesc;
|
||||||
String? _event;
|
String? _event;
|
||||||
@ -91,6 +95,7 @@ class SCEditRoomInfoRes {
|
|||||||
String? roomAccount,
|
String? roomAccount,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
String? event,
|
String? event,
|
||||||
@ -108,6 +113,7 @@ class SCEditRoomInfoRes {
|
|||||||
roomAccount: roomAccount ?? _roomAccount,
|
roomAccount: roomAccount ?? _roomAccount,
|
||||||
userId: userId ?? _userId,
|
userId: userId ?? _userId,
|
||||||
roomCover: roomCover ?? _roomCover,
|
roomCover: roomCover ?? _roomCover,
|
||||||
|
roomBackground: roomBackground ?? _roomBackground,
|
||||||
roomName: roomName ?? _roomName,
|
roomName: roomName ?? _roomName,
|
||||||
roomDesc: roomDesc ?? _roomDesc,
|
roomDesc: roomDesc ?? _roomDesc,
|
||||||
event: event ?? _event,
|
event: event ?? _event,
|
||||||
@ -125,6 +131,7 @@ class SCEditRoomInfoRes {
|
|||||||
String? get roomAccount => _roomAccount;
|
String? get roomAccount => _roomAccount;
|
||||||
String? get userId => _userId;
|
String? get userId => _userId;
|
||||||
String? get roomCover => _roomCover;
|
String? get roomCover => _roomCover;
|
||||||
|
String? get roomBackground => _roomBackground;
|
||||||
String? get roomName => _roomName;
|
String? get roomName => _roomName;
|
||||||
String? get roomDesc => _roomDesc;
|
String? get roomDesc => _roomDesc;
|
||||||
String? get event => _event;
|
String? get event => _event;
|
||||||
@ -144,6 +151,7 @@ class SCEditRoomInfoRes {
|
|||||||
map['roomAccount'] = _roomAccount;
|
map['roomAccount'] = _roomAccount;
|
||||||
map['userId'] = _userId;
|
map['userId'] = _userId;
|
||||||
map['roomCover'] = _roomCover;
|
map['roomCover'] = _roomCover;
|
||||||
|
map['roomBackground'] = _roomBackground;
|
||||||
map['roomName'] = _roomName;
|
map['roomName'] = _roomName;
|
||||||
map['roomDesc'] = _roomDesc;
|
map['roomDesc'] = _roomDesc;
|
||||||
map['event'] = _event;
|
map['event'] = _event;
|
||||||
|
|||||||
@ -90,6 +90,12 @@ abstract class SocialChatUserRepository {
|
|||||||
String event,
|
String event,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
///修改房间背景图
|
||||||
|
Future<SCEditRoomInfoRes> updateRoomBackground(
|
||||||
|
String roomId,
|
||||||
|
String roomBackground,
|
||||||
|
);
|
||||||
|
|
||||||
///获取指定用户信息
|
///获取指定用户信息
|
||||||
Future<SocialChatUserProfile> loadUserInfo(String userId);
|
Future<SocialChatUserProfile> loadUserInfo(String userId);
|
||||||
|
|
||||||
|
|||||||
@ -76,6 +76,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
specificRoom.roomCover,
|
specificRoom.roomCover,
|
||||||
cachedRoom.roomCover,
|
cachedRoom.roomCover,
|
||||||
),
|
),
|
||||||
|
roomBackground: SCRoomProfileCache.preferNonEmpty(
|
||||||
|
specificRoom.roomBackground,
|
||||||
|
cachedRoom.roomBackground,
|
||||||
|
),
|
||||||
roomName: SCRoomProfileCache.preferNonEmpty(
|
roomName: SCRoomProfileCache.preferNonEmpty(
|
||||||
specificRoom.roomName,
|
specificRoom.roomName,
|
||||||
cachedRoom.roomName,
|
cachedRoom.roomName,
|
||||||
@ -137,11 +141,12 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
fromJson: (json) => MyRoomRes.fromJson(json),
|
fromJson: (json) => MyRoomRes.fromJson(json),
|
||||||
);
|
);
|
||||||
debugPrint(
|
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(
|
await SCRoomProfileCache.saveRoomProfile(
|
||||||
roomId: result.id ?? roomId,
|
roomId: result.id ?? roomId,
|
||||||
roomCover: result.roomCover,
|
roomCover: result.roomCover,
|
||||||
|
roomBackground: result.roomBackground,
|
||||||
roomName: result.roomName,
|
roomName: result.roomName,
|
||||||
roomDesc: result.roomDesc,
|
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/models/res/sc_violation_handle_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/repositories/user_repository.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/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';
|
import 'package:yumi/shared/tools/sc_room_profile_cache.dart';
|
||||||
|
|
||||||
class SCAccountRepository implements SocialChatUserRepository {
|
class SCAccountRepository implements SocialChatUserRepository {
|
||||||
@ -81,9 +82,11 @@ class SCAccountRepository implements SocialChatUserRepository {
|
|||||||
///auth/account/login
|
///auth/account/login
|
||||||
@override
|
@override
|
||||||
Future<SocialChatLoginRes> loginForAccount(String account, String pwd) async {
|
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>(
|
final result = await http.post<SocialChatLoginRes>(
|
||||||
"e64f27b9ba6b37881120f4584a5444a5c684d8491b703d0af953e5cc6a5f4cec",
|
"e64f27b9ba6b37881120f4584a5444a5c684d8491b703d0af953e5cc6a5f4cec",
|
||||||
data: {"account": account, "pwd": pwd},
|
data: data,
|
||||||
fromJson: (json) => SocialChatLoginRes.fromJson(json),
|
fromJson: (json) => SocialChatLoginRes.fromJson(json),
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
@ -95,9 +98,11 @@ class SCAccountRepository implements SocialChatUserRepository {
|
|||||||
String authType,
|
String authType,
|
||||||
String openId,
|
String openId,
|
||||||
) async {
|
) async {
|
||||||
|
final data = <String, dynamic>{"authType": authType, "openId": openId};
|
||||||
|
data.addAll(await SCMobileLoginContext.loginBodyFields());
|
||||||
final result = await http.post<SocialChatLoginRes>(
|
final result = await http.post<SocialChatLoginRes>(
|
||||||
"e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44",
|
"e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44",
|
||||||
data: {"authType": authType, "openId": openId},
|
data: data,
|
||||||
fromJson: (json) => SocialChatLoginRes.fromJson(json),
|
fromJson: (json) => SocialChatLoginRes.fromJson(json),
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
@ -275,6 +280,34 @@ class SCAccountRepository implements SocialChatUserRepository {
|
|||||||
return result;
|
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
|
///user/user-profile
|
||||||
@override
|
@override
|
||||||
Future<SocialChatUserProfile> loadUserInfo(String userId) async {
|
Future<SocialChatUserProfile> loadUserInfo(String userId) async {
|
||||||
|
|||||||
@ -61,7 +61,12 @@ class SCGiftVapSvgaManager {
|
|||||||
|
|
||||||
void setMute(bool muteMusic) {
|
void setMute(bool muteMusic) {
|
||||||
_mute = 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() {
|
bool getMute() {
|
||||||
@ -415,6 +420,7 @@ class SCGiftVapSvgaManager {
|
|||||||
_mute = DataPersistence.getPlayGiftMusic();
|
_mute = DataPersistence.getPlayGiftMusic();
|
||||||
_dis = false;
|
_dis = false;
|
||||||
_rgc = vapController;
|
_rgc = vapController;
|
||||||
|
unawaited(vapController.setMute(_mute));
|
||||||
_log(
|
_log(
|
||||||
'bindVapCtrl hasVapCtrl=${_rgc != null} '
|
'bindVapCtrl hasVapCtrl=${_rgc != null} '
|
||||||
'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute',
|
'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute',
|
||||||
@ -439,8 +445,10 @@ class SCGiftVapSvgaManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void bindSvgaCtrl(SVGAAnimationController svgaController) {
|
void bindSvgaCtrl(SVGAAnimationController svgaController) {
|
||||||
|
_mute = DataPersistence.getPlayGiftMusic();
|
||||||
_dis = false;
|
_dis = false;
|
||||||
_rsc = svgaController;
|
_rsc = svgaController;
|
||||||
|
_rsc?.muted = _mute;
|
||||||
_log(
|
_log(
|
||||||
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
|
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
|
||||||
'hasVapCtrl=${_rgc != null} queue=$_queueSummary',
|
'hasVapCtrl=${_rgc != null} queue=$_queueSummary',
|
||||||
@ -565,6 +573,7 @@ class SCGiftVapSvgaManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('use prepared asset svga path=${task.path}');
|
_log('use prepared asset svga path=${task.path}');
|
||||||
|
_rsc?.muted = _mute;
|
||||||
_rsc?.videoItem = entity;
|
_rsc?.videoItem = entity;
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
_rsc?.forward();
|
_rsc?.forward();
|
||||||
@ -591,6 +600,7 @@ class SCGiftVapSvgaManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('play local svga file path=${task.path}');
|
_log('play local svga file path=${task.path}');
|
||||||
|
_rsc?.muted = _mute;
|
||||||
_rsc?.videoItem = entity;
|
_rsc?.videoItem = entity;
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
_rsc?.forward();
|
_rsc?.forward();
|
||||||
@ -634,6 +644,7 @@ class SCGiftVapSvgaManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('use prepared network svga path=${task.path}');
|
_log('use prepared network svga path=${task.path}');
|
||||||
|
_rsc?.muted = _mute;
|
||||||
_rsc?.videoItem = entity;
|
_rsc?.videoItem = entity;
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
_rsc?.forward();
|
_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({
|
static Future<void> saveRoomProfile({
|
||||||
required String roomId,
|
required String roomId,
|
||||||
String? roomCover,
|
String? roomCover,
|
||||||
|
String? roomBackground,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? roomDesc,
|
String? roomDesc,
|
||||||
}) async {
|
}) async {
|
||||||
@ -66,6 +67,8 @@ class SCRoomProfileCache {
|
|||||||
final current = getRoomProfile(roomId);
|
final current = getRoomProfile(roomId);
|
||||||
final payload = <String, String>{
|
final payload = <String, String>{
|
||||||
"roomCover": preferNonEmpty(roomCover, current["roomCover"]) ?? "",
|
"roomCover": preferNonEmpty(roomCover, current["roomCover"]) ?? "",
|
||||||
|
"roomBackground":
|
||||||
|
preferNonEmpty(roomBackground, current["roomBackground"]) ?? "",
|
||||||
"roomName": preferNonEmpty(roomName, current["roomName"]) ?? "",
|
"roomName": preferNonEmpty(roomName, current["roomName"]) ?? "",
|
||||||
"roomDesc": preferNonEmpty(roomDesc, current["roomDesc"]) ?? "",
|
"roomDesc": preferNonEmpty(roomDesc, current["roomDesc"]) ?? "",
|
||||||
};
|
};
|
||||||
@ -79,6 +82,10 @@ class SCRoomProfileCache {
|
|||||||
}
|
}
|
||||||
return room.copyWith(
|
return room.copyWith(
|
||||||
roomCover: preferCachedValue(cache["roomCover"], room.roomCover),
|
roomCover: preferCachedValue(cache["roomCover"], room.roomCover),
|
||||||
|
roomBackground: preferCachedValue(
|
||||||
|
cache["roomBackground"],
|
||||||
|
room.roomBackground,
|
||||||
|
),
|
||||||
roomName: preferCachedValue(cache["roomName"], room.roomName),
|
roomName: preferCachedValue(cache["roomName"], room.roomName),
|
||||||
roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc),
|
roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc),
|
||||||
);
|
);
|
||||||
@ -96,6 +103,10 @@ class SCRoomProfileCache {
|
|||||||
return room.copyWith(
|
return room.copyWith(
|
||||||
roomProfile: roomProfile.copyWith(
|
roomProfile: roomProfile.copyWith(
|
||||||
roomCover: preferCachedValue(cache["roomCover"], roomProfile.roomCover),
|
roomCover: preferCachedValue(cache["roomCover"], roomProfile.roomCover),
|
||||||
|
roomBackground: preferCachedValue(
|
||||||
|
cache["roomBackground"],
|
||||||
|
roomProfile.roomBackground,
|
||||||
|
),
|
||||||
roomName: preferCachedValue(cache["roomName"], roomProfile.roomName),
|
roomName: preferCachedValue(cache["roomName"], roomProfile.roomName),
|
||||||
roomDesc: preferCachedValue(cache["roomDesc"], roomProfile.roomDesc),
|
roomDesc: preferCachedValue(cache["roomDesc"], roomProfile.roomDesc),
|
||||||
),
|
),
|
||||||
@ -109,6 +120,10 @@ class SCRoomProfileCache {
|
|||||||
}
|
}
|
||||||
return room.copyWith(
|
return room.copyWith(
|
||||||
roomCover: preferCachedValue(cache["roomCover"], room.roomCover),
|
roomCover: preferCachedValue(cache["roomCover"], room.roomCover),
|
||||||
|
roomBackground: preferCachedValue(
|
||||||
|
cache["roomBackground"],
|
||||||
|
room.roomBackground,
|
||||||
|
),
|
||||||
roomName: preferCachedValue(cache["roomName"], room.roomName),
|
roomName: preferCachedValue(cache["roomName"], room.roomName),
|
||||||
roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc),
|
roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc),
|
||||||
);
|
);
|
||||||
@ -131,6 +146,10 @@ class SCRoomProfileCache {
|
|||||||
cache["roomCover"],
|
cache["roomCover"],
|
||||||
basicRoomProfile.roomCover,
|
basicRoomProfile.roomCover,
|
||||||
),
|
),
|
||||||
|
roomBackground: preferCachedValue(
|
||||||
|
cache["roomBackground"],
|
||||||
|
basicRoomProfile.roomBackground,
|
||||||
|
),
|
||||||
roomName: preferCachedValue(
|
roomName: preferCachedValue(
|
||||||
cache["roomName"],
|
cache["roomName"],
|
||||||
basicRoomProfile.roomName,
|
basicRoomProfile.roomName,
|
||||||
|
|||||||
@ -21,16 +21,34 @@ class LGiftAnimalPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
|
static const double _tickerHeight = 52;
|
||||||
|
static const double _slotExtent = 70;
|
||||||
|
static const double _entryOffset = 60;
|
||||||
|
|
||||||
late final GiftAnimationManager _giftAnimationManager;
|
late final GiftAnimationManager _giftAnimationManager;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return SizedBox(
|
||||||
height: 332.w,
|
height: _requiredHeightForSlots(visibleSlotCount),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
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() {
|
void initAnimal() {
|
||||||
List<LGiftScrollingScreenAnimsBean> beans = [];
|
List<LGiftScrollingScreenAnimsBean> beans = [];
|
||||||
double top = 60;
|
for (int i = 0; i < GiftAnimationManager.maxVisibleSlots; i++) {
|
||||||
for (int i = 0; i < 4; i++) {
|
final targetTop = i * _slotExtent;
|
||||||
var bean = LGiftScrollingScreenAnimsBean();
|
var bean = LGiftScrollingScreenAnimsBean();
|
||||||
var controller = AnimationController(
|
var controller = AnimationController(
|
||||||
value: 0,
|
value: 0,
|
||||||
@ -66,10 +84,10 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
|||||||
duration: const Duration(milliseconds: 280),
|
duration: const Duration(milliseconds: 280),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top);
|
bean.luckyGiftPinnedMargin = EdgeInsets.only(top: targetTop.w);
|
||||||
bean.verticalAnimation = EdgeInsetsTween(
|
bean.verticalAnimation = EdgeInsetsTween(
|
||||||
begin: EdgeInsets.only(top: top),
|
begin: EdgeInsets.only(top: (targetTop + _entryOffset).w),
|
||||||
end: EdgeInsets.only(top: 0),
|
end: EdgeInsets.only(top: targetTop.w),
|
||||||
).animate(
|
).animate(
|
||||||
CurvedAnimation(parent: controller, curve: Curves.easeOutCubic),
|
CurvedAnimation(parent: controller, curve: Curves.easeOutCubic),
|
||||||
);
|
);
|
||||||
@ -95,11 +113,36 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
beans.add(bean);
|
beans.add(bean);
|
||||||
top = top + 70;
|
|
||||||
}
|
}
|
||||||
_giftAnimationManager.attachAnimationControllers(beans);
|
_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) {
|
String getBg(String type) {
|
||||||
return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png";
|
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/app/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/services/general/sc_app_general_manager.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_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/shared/tools/sc_room_utils.dart';
|
||||||
import 'package:yumi/main.dart';
|
import 'package:yumi/main.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||||
@ -83,6 +84,17 @@ class _FloatingLuckGiftScreenWidgetState
|
|||||||
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
end: const Offset(-1.5, 0.0), // 到屏幕左侧外
|
||||||
).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn));
|
).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) {
|
_controller.addStatusListener((status) {
|
||||||
if (status == AnimationStatus.completed && !_isSwipeAnimating) {
|
if (status == AnimationStatus.completed && !_isSwipeAnimating) {
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import '../../components/sc_debounce_widget.dart';
|
|||||||
class RoomBottomWidget extends StatefulWidget {
|
class RoomBottomWidget extends StatefulWidget {
|
||||||
const RoomBottomWidget({super.key, this.showGiftComboButton = true});
|
const RoomBottomWidget({super.key, this.showGiftComboButton = true});
|
||||||
|
|
||||||
|
static const double floatingButtonHostHeight = _floatingButtonHostHeight;
|
||||||
|
|
||||||
final bool showGiftComboButton;
|
final bool showGiftComboButton;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class _RoomMenuDialogState extends State<RoomMenuDialog> {
|
|||||||
static const int _menuReport = 5;
|
static const int _menuReport = 5;
|
||||||
static const int _menuBackground = 6;
|
static const int _menuBackground = 6;
|
||||||
static const int _menuMusic = 7;
|
static const int _menuMusic = 7;
|
||||||
static bool get _showBackgroundFeature => false;
|
static bool get _showBackgroundFeature => true;
|
||||||
|
|
||||||
List<RoomMenu> items2 = [];
|
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
|
source: hosted
|
||||||
version: "2.13.1"
|
version: "2.13.1"
|
||||||
audioplayers:
|
audioplayers:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: audioplayers
|
name: audioplayers
|
||||||
sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70
|
sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70
|
||||||
@ -638,10 +638,9 @@ packages:
|
|||||||
flutter_svga:
|
flutter_svga:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svga
|
path: "local_packages/flutter_svga-0.0.13-patched"
|
||||||
sha256: c914ba2aee5e2d53775b64c7ff6530b4cdc3c27fd9348debb0d86fd68b869c39
|
relative: true
|
||||||
url: "https://pub.flutter-io.cn"
|
source: path
|
||||||
source: hosted
|
|
||||||
version: "0.0.13"
|
version: "0.0.13"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
|
|||||||
@ -96,7 +96,8 @@ dependencies:
|
|||||||
tancent_vap:
|
tancent_vap:
|
||||||
path: ./local_packages/tancent_vap-1.0.0+1
|
path: ./local_packages/tancent_vap-1.0.0+1
|
||||||
#svga特效
|
#svga特效
|
||||||
flutter_svga: ^0.0.5
|
flutter_svga:
|
||||||
|
path: ./local_packages/flutter_svga-0.0.13-patched
|
||||||
#banner
|
#banner
|
||||||
carousel_slider: ^5.1.1
|
carousel_slider: ^5.1.1
|
||||||
#谷歌支付
|
#谷歌支付
|
||||||
@ -115,6 +116,7 @@ dependencies:
|
|||||||
#图片压缩
|
#图片压缩
|
||||||
flutter_image_compress: ^2.4.0
|
flutter_image_compress: ^2.4.0
|
||||||
extended_text: ^15.0.2
|
extended_text: ^15.0.2
|
||||||
|
audioplayers: ^6.5.1
|
||||||
#缩略图
|
#缩略图
|
||||||
video_thumbnail: ^0.5.6
|
video_thumbnail: ^0.5.6
|
||||||
badges: ^3.1.2
|
badges: ^3.1.2
|
||||||
@ -164,6 +166,7 @@ flutter:
|
|||||||
- sc_images/daily_sign_in/
|
- sc_images/daily_sign_in/
|
||||||
- sc_images/register_reward/
|
- sc_images/register_reward/
|
||||||
- sc_images/room/
|
- sc_images/room/
|
||||||
|
- sc_images/room/background_examples/
|
||||||
- sc_images/room/emoji/
|
- sc_images/room/emoji/
|
||||||
- sc_images/room/entrance/
|
- sc_images/room/entrance/
|
||||||
- sc_images/room/anim/
|
- 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 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。
|
- 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。
|
||||||
- 按桌面《Android音乐添加流程.md》在 Android 语言房内补齐本地 MP3 音乐功能,并持续记录素材接入、扫描添加、列表管理、播放器和房间播放链路进度。
|
- 按桌面《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 语言房音乐功能(进行中)
|
||||||
- 已查阅桌面《Android音乐添加流程.md》,并产出可执行方案文档 `docs/android-room-music-execution-plan.md`,覆盖功能范围、模块拆分、Android 权限、声网混音链路、UI 流程、风险点和 GPT 5.5 预估开发工时。
|
- 已查阅桌面《Android音乐添加流程.md》,并产出可执行方案文档 `docs/android-room-music-execution-plan.md`,覆盖功能范围、模块拆分、Android 权限、声网混音链路、UI 流程、风险点和 GPT 5.5 预估开发工时。
|
||||||
- 已导入桌面 `音乐素材` 文件夹内的 12 张音乐相关 PNG,并统一落到 `sc_images/room/sc_music_material_*`,当前用于房间菜单入口、播放器按钮、模式切换、音量、文件夹、更多、删除和编辑等 UI 位。
|
- 已导入桌面 `音乐素材` 文件夹内的 12 张音乐相关 PNG,并统一落到 `sc_images/room/sc_music_material_*`,当前用于房间菜单入口、播放器按钮、模式切换、音量、文件夹、更多、删除和编辑等 UI 位。
|
||||||
|
|||||||